1use anyhow::{Context, Result};
9use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyModifiers, MouseEvent, MouseEventKind};
10use ratatui::{
11 Frame, Terminal,
12 backend::CrosstermBackend,
13 layout::{Constraint, Direction, Layout, Rect},
14 style::{Color, Modifier, Style},
15 text::{Line, Span},
16 widgets::{Block, Borders, Paragraph},
17};
18use std::io;
19use std::sync::mpsc::{self, Receiver, Sender};
20use std::time::Duration;
21use textwrap;
22
23use crate::cache::CacheManager;
24
25use super::AgenticConfig;
26use super::chat_session::{ChatSession, MessageRole};
27
28#[derive(Debug, Clone)]
30enum PhaseUpdate {
31 Triaging,
33
34 AnsweringFromContext,
36
37 Thinking {
39 reasoning: String,
40 needs_context: bool,
41 },
42 Tools {
44 content: String,
45 tool_calls: Vec<String>,
46 },
47 Queries { queries: Vec<String> },
49 Executing {
51 results_count: usize,
52 execution_time_ms: u64,
53 },
54 #[allow(dead_code)]
56 Reindexing {
57 current: usize,
58 total: usize,
59 message: String,
60 },
61 Answer { answer: String },
63 Error { error: String },
65 Notice { message: String },
69 Done,
71}
72
73#[derive(Debug, Clone)]
75enum TriageDecision {
76 DirectAnswer,
78 NeedsSearch { reasoning: String },
80}
81
82fn wrap_with_prefix(content: &str, area_width: u16, border_color: Color) -> Vec<Line<'_>> {
84 let mut lines = Vec::new();
85
86 let usable_width = (area_width.saturating_sub(4)) as usize;
88
89 if usable_width < 10 {
91 for content_line in content.lines() {
93 lines.push(Line::from(vec![
94 Span::styled("│ ", Style::default().fg(border_color)),
95 Span::styled(content_line, Style::default().fg(Color::White)),
96 ]));
97 }
98 return lines;
99 }
100
101 for content_line in content.lines() {
103 if content_line.is_empty() {
104 lines.push(Line::from(Span::styled(
106 "│ ",
107 Style::default().fg(border_color),
108 )));
109 } else {
110 let wrapped = textwrap::wrap(content_line, usable_width);
112 for wrapped_line in wrapped {
113 lines.push(Line::from(vec![
114 Span::styled("│ ", Style::default().fg(border_color)),
115 Span::styled(wrapped_line.to_string(), Style::default().fg(Color::White)),
116 ]));
117 }
118 }
119 }
120
121 lines
122}
123
124fn render_markdown_with_prefix(
126 content: &str,
127 area_width: u16,
128 border_color: Color,
129) -> Vec<Line<'static>> {
130 let mut lines = Vec::new();
131
132 let usable_width = (area_width.saturating_sub(4)) as usize;
134
135 if usable_width < 10 {
137 for content_line in content.lines() {
139 lines.push(Line::from(vec![
140 Span::styled("│ ", Style::default().fg(border_color)),
141 Span::styled(content_line.to_string(), Style::default().fg(Color::White)),
142 ]));
143 }
144 return lines;
145 }
146
147 let mut in_code_block = false;
148
149 for content_line in content.lines() {
150 if content_line.trim().starts_with("```") {
152 in_code_block = !in_code_block;
153 continue; }
155
156 if in_code_block {
157 lines.push(Line::from(vec![
160 Span::styled("│ ", Style::default().fg(border_color)),
161 Span::styled(
162 content_line.to_string(),
163 Style::default().fg(Color::Cyan).bg(Color::Black),
164 ),
165 ]));
166 continue;
167 }
168
169 if content_line.is_empty() {
170 lines.push(Line::from(Span::styled(
171 "│ ",
172 Style::default().fg(border_color),
173 )));
174 continue;
175 }
176
177 let (header_level, text_after_header) = if let Some(s) = content_line.strip_prefix("### ") {
179 (3, s)
180 } else if let Some(s) = content_line.strip_prefix("## ") {
181 (2, s)
182 } else if let Some(s) = content_line.strip_prefix("# ") {
183 (1, s)
184 } else {
185 (0, content_line)
186 };
187
188 let wrapped = textwrap::wrap(text_after_header, usable_width);
189
190 for wrapped_line in wrapped {
191 let parsed_spans = parse_inline_markdown(&wrapped_line);
192
193 let mut line_spans = vec![Span::styled("│ ", Style::default().fg(border_color))];
195
196 if header_level > 0 {
198 for span in parsed_spans {
199 let mut style = span.style;
200 style = style.add_modifier(Modifier::BOLD);
201 if header_level == 1 {
202 style = style.fg(Color::Yellow);
203 } else if header_level == 2 {
204 style = style.fg(Color::Cyan);
205 }
206 line_spans.push(Span::styled(span.content.to_string(), style));
207 }
208 } else {
209 line_spans.extend(
210 parsed_spans
211 .into_iter()
212 .map(|s| Span::styled(s.content.to_string(), s.style)),
213 );
214 }
215
216 lines.push(Line::from(line_spans));
217 }
218 }
219
220 lines
221}
222
223fn parse_inline_markdown(text: &str) -> Vec<Span<'static>> {
225 let mut result = Vec::new();
226 let chars: Vec<char> = text.chars().collect();
227 let mut i = 0;
228
229 while i < chars.len() {
230 if i + 1 < chars.len()
232 && chars[i] == '*'
233 && chars[i + 1] == '*'
234 && let Some(end) = find_closing_double_star(&chars, i + 2)
235 {
236 let content: String = chars[i + 2..end].iter().collect();
237 result.push(Span::styled(
238 content,
239 Style::default()
240 .fg(Color::White)
241 .add_modifier(Modifier::BOLD),
242 ));
243 i = end + 2;
244 continue;
245 }
246
247 if chars[i] == '*' || chars[i] == '_' {
249 let marker = chars[i];
250 if let Some(end) = find_closing_char(&chars, i + 1, marker) {
251 let content: String = chars[i + 1..end].iter().collect();
252 result.push(Span::styled(
253 content,
254 Style::default()
255 .fg(Color::White)
256 .add_modifier(Modifier::ITALIC),
257 ));
258 i = end + 1;
259 continue;
260 }
261 }
262
263 if chars[i] == '`'
265 && let Some(end) = find_closing_char(&chars, i + 1, '`')
266 {
267 let content: String = chars[i + 1..end].iter().collect();
268 result.push(Span::styled(
269 content,
270 Style::default().fg(Color::Cyan).bg(Color::Black),
271 ));
272 i = end + 1;
273 continue;
274 }
275
276 let mut plain_text = String::new();
278 while i < chars.len() && chars[i] != '*' && chars[i] != '_' && chars[i] != '`' {
279 plain_text.push(chars[i]);
280 i += 1;
281 }
282
283 if !plain_text.is_empty() {
284 result.push(Span::styled(plain_text, Style::default().fg(Color::White)));
285 }
286
287 if i < chars.len() && (chars[i] == '*' || chars[i] == '_' || chars[i] == '`') {
290 result.push(Span::styled(
291 chars[i].to_string(),
292 Style::default().fg(Color::White),
293 ));
294 i += 1; }
296 }
297
298 if result.is_empty() {
299 result.push(Span::raw(""));
300 }
301
302 result
303}
304
305fn find_closing_double_star(chars: &[char], start: usize) -> Option<usize> {
307 (start..chars.len().saturating_sub(1)).find(|&i| chars[i] == '*' && chars[i + 1] == '*')
308}
309
310fn find_closing_char(chars: &[char], start: usize, marker: char) -> Option<usize> {
312 for i in start..chars.len() {
313 if chars[i] == marker {
314 if marker == '*' && i + 1 < chars.len() && chars[i + 1] == '*' {
316 continue; }
318 return Some(i);
319 }
320 }
321 None
322}
323
324pub struct ChatApp {
326 session: ChatSession,
328
329 input: String,
331
332 cursor: usize,
334
335 scroll_offset: usize,
337
338 should_quit: bool,
340
341 cache: CacheManager,
343
344 provider_name: String,
346
347 model_override: Option<String>,
349
350 status_message: Option<String>,
352
353 waiting: bool,
355
356 progress_rx: Option<Receiver<PhaseUpdate>>,
358
359 spinner_frame: usize,
361}
362
363impl ChatApp {
364 pub fn new(
366 cache: CacheManager,
367 provider_name: String,
368 model_override: Option<String>,
369 ) -> Result<Self> {
370 let model =
375 super::config::resolve_model_for(&provider_name, None, model_override.as_deref())
376 .or_else(|| {
377 let d = super::providers::default_model_for(&provider_name);
378 if d.is_empty() {
379 None
380 } else {
381 Some(d.to_string())
382 }
383 })
384 .unwrap_or_else(|| "(not configured)".to_string());
385
386 let session = ChatSession::new(provider_name.clone(), model);
387
388 Ok(Self {
389 session,
390 input: String::new(),
391 cursor: 0,
392 scroll_offset: 0,
393 should_quit: false,
394 cache,
395 provider_name,
396 model_override,
397 status_message: None,
398 waiting: false,
399 progress_rx: None,
400 spinner_frame: 0,
401 })
402 }
403
404 pub fn run(&mut self) -> Result<()> {
406 let mut terminal = setup_terminal()?;
408
409 self.session.add_system_message(
411 "Welcome to rfx ask interactive mode!\n\
412 \n\
413 Type your questions naturally and press Enter to send.\n\
414 \n\
415 Slash commands:\n\
416 • /clear - Clear conversation history\n\
417 • /compact - Summarize old messages to save tokens\n\
418 • /model [provider] [model] - Show or change provider/model\n\
419 • /help - Show this help message\n\
420 \n\
421 Press Ctrl+C to exit."
422 .to_string(),
423 );
424
425 let result = self.event_loop(&mut terminal);
427
428 restore_terminal(terminal)?;
430
431 result
432 }
433
434 fn event_loop(&mut self, terminal: &mut Terminal<CrosstermBackend<io::Stdout>>) -> Result<()> {
435 loop {
436 let updates: Vec<PhaseUpdate> = if let Some(ref rx) = self.progress_rx {
439 let mut updates = Vec::new();
440 while let Ok(update) = rx.try_recv() {
442 updates.push(update);
443 }
444 updates
445 } else {
446 Vec::new()
447 };
448
449 for update in updates {
451 self.handle_progress_update(update);
452 }
453
454 if self.waiting {
456 self.spinner_frame = (self.spinner_frame + 1) % 10;
457 }
458
459 terminal.draw(|f| self.render(f))?;
461
462 if event::poll(Duration::from_millis(100))? {
464 match event::read()? {
465 Event::Key(key) => self.handle_key(key)?,
466 Event::Mouse(mouse) => self.handle_mouse(mouse),
467 _ => {}
468 }
469 }
470
471 if self.should_quit {
472 break;
473 }
474 }
475
476 Ok(())
477 }
478
479 fn render(&mut self, f: &mut Frame) {
480 let size = f.area();
481
482 let chunks = Layout::default()
484 .direction(Direction::Vertical)
485 .constraints([
486 Constraint::Length(2), Constraint::Min(10), Constraint::Length(4), ])
490 .split(size);
491
492 self.render_stats(f, chunks[0]);
494 self.render_messages(f, chunks[1]);
495 self.render_input(f, chunks[2]);
496 }
497
498 fn render_stats(&self, f: &mut Frame, area: Rect) {
499 let usage = self.session.context_usage();
500 let percentage = (usage * 100.0) as u32;
501
502 let usage_color = if usage > 0.9 {
504 Color::Red
505 } else if usage > 0.8 {
506 Color::Yellow
507 } else {
508 Color::Green
509 };
510
511 let line1 = Line::from(vec![
512 Span::raw("Model: "),
513 Span::styled(
514 format!("{} ", self.session.model()),
515 Style::default()
516 .fg(Color::Cyan)
517 .add_modifier(Modifier::BOLD),
518 ),
519 Span::raw("│ Provider: "),
520 Span::styled(
521 format!("{} ", self.session.provider()),
522 Style::default().fg(Color::Blue),
523 ),
524 Span::raw("│ Tokens: "),
525 Span::styled(
526 format!(
527 "{}/{} ",
528 self.session.total_tokens(),
529 self.session.context_limit()
530 ),
531 Style::default()
532 .fg(usage_color)
533 .add_modifier(Modifier::BOLD),
534 ),
535 Span::styled(
536 format!("({}%)", percentage),
537 Style::default().fg(usage_color),
538 ),
539 ]);
540
541 let line2_text = if let Some(ref status) = self.status_message {
543 status.clone()
544 } else if self.waiting {
545 "⏳ Waiting for response...".to_string()
546 } else if self.session.should_compact() {
547 "⚠ Context >90% full! Use /compact to summarize older messages.".to_string()
548 } else if self.session.is_near_limit() {
549 "⚠ Context >80% full. Consider using /compact soon.".to_string()
550 } else {
551 "Ready • Type your question or /help for commands".to_string()
552 };
553
554 let line2_color = if self.session.should_compact() {
555 Color::Red
556 } else if self.session.is_near_limit() {
557 Color::Yellow
558 } else if self.waiting {
559 Color::Cyan
560 } else {
561 Color::Gray
562 };
563
564 let line2 = Line::from(Span::styled(line2_text, Style::default().fg(line2_color)));
565
566 let paragraph = Paragraph::new(vec![line1, line2]).style(Style::default().bg(Color::Black));
567
568 f.render_widget(paragraph, area);
569 }
570
571 fn render_messages(&mut self, f: &mut Frame, area: Rect) {
572 let mut lines: Vec<Line> = Vec::new();
573
574 for msg in self.session.messages() {
576 match msg.role {
577 MessageRole::User => {
578 lines.push(Line::from(""));
580 lines.push(Line::from(Span::styled(
581 "╭─ You ─────────────────────────────────────",
582 Style::default()
583 .fg(Color::Green)
584 .add_modifier(Modifier::BOLD),
585 )));
586
587 lines.extend(wrap_with_prefix(&msg.content, area.width, Color::Green));
589
590 lines.push(Line::from(Span::styled(
591 "╰───────────────────────────────────────────",
592 Style::default().fg(Color::Green),
593 )));
594 }
595 MessageRole::AssistantThinking => {
596 lines.push(Line::from(""));
598 lines.push(Line::from(Span::styled(
599 "╭─ Assistant (Thinking) ────────────────────",
600 Style::default()
601 .fg(Color::Magenta)
602 .add_modifier(Modifier::BOLD),
603 )));
604
605 if let Some(ref meta) = msg.metadata
607 && meta.needs_context
608 {
609 lines.push(Line::from(vec![
610 Span::styled("│ ", Style::default().fg(Color::Magenta)),
611 Span::styled(
612 "🔍 Needs context gathering",
613 Style::default().fg(Color::Yellow),
614 ),
615 ]));
616 }
617
618 lines.extend(wrap_with_prefix(&msg.content, area.width, Color::Magenta));
620
621 lines.push(Line::from(Span::styled(
622 "╰───────────────────────────────────────────",
623 Style::default().fg(Color::Magenta),
624 )));
625 }
626 MessageRole::AssistantTools => {
627 lines.push(Line::from(""));
629 lines.push(Line::from(Span::styled(
630 "╭─ Assistant (Tools) ───────────────────────",
631 Style::default()
632 .fg(Color::Blue)
633 .add_modifier(Modifier::BOLD),
634 )));
635
636 if let Some(ref meta) = msg.metadata
638 && !meta.tool_calls.is_empty()
639 {
640 lines.push(Line::from(vec![
641 Span::styled("│ ", Style::default().fg(Color::Blue)),
642 Span::styled(
643 format!("🔧 {} tool calls made", meta.tool_calls.len()),
644 Style::default().fg(Color::DarkGray),
645 ),
646 ]));
647 }
648
649 lines.extend(wrap_with_prefix(&msg.content, area.width, Color::Blue));
651
652 lines.push(Line::from(Span::styled(
653 "╰───────────────────────────────────────────",
654 Style::default().fg(Color::Blue),
655 )));
656 }
657 MessageRole::AssistantQueries => {
658 lines.push(Line::from(""));
660 lines.push(Line::from(Span::styled(
661 "╭─ Assistant (Queries) ─────────────────────",
662 Style::default()
663 .fg(Color::Magenta)
664 .add_modifier(Modifier::BOLD),
665 )));
666
667 if let Some(ref meta) = msg.metadata
669 && !meta.queries.is_empty()
670 {
671 lines.push(Line::from(vec![
672 Span::styled("│ ", Style::default().fg(Color::Magenta)),
673 Span::styled(
674 format!("📝 Generated {} queries", meta.queries.len()),
675 Style::default().fg(Color::DarkGray),
676 ),
677 ]));
678 for (i, query) in meta.queries.iter().enumerate() {
680 lines.push(Line::from(vec![
681 Span::styled("│ ", Style::default().fg(Color::Magenta)),
682 Span::styled(
683 format!(" {}. {}", i + 1, query),
684 Style::default().fg(Color::DarkGray),
685 ),
686 ]));
687 }
688 }
689
690 lines.extend(wrap_with_prefix(&msg.content, area.width, Color::Magenta));
692
693 lines.push(Line::from(Span::styled(
694 "╰───────────────────────────────────────────",
695 Style::default().fg(Color::Magenta),
696 )));
697 }
698 MessageRole::AssistantExecuting => {
699 lines.push(Line::from(""));
701 lines.push(Line::from(Span::styled(
702 "╭─ Assistant (Executing) ───────────────────",
703 Style::default()
704 .fg(Color::Yellow)
705 .add_modifier(Modifier::BOLD),
706 )));
707
708 if let Some(ref meta) = msg.metadata {
710 let time_str = if let Some(ms) = meta.execution_time_ms {
711 format!(" in {}ms", ms)
712 } else {
713 String::new()
714 };
715 lines.push(Line::from(vec![
716 Span::styled("│ ", Style::default().fg(Color::Yellow)),
717 Span::styled(
718 format!(
719 "⚡ Found {} result{}{}",
720 meta.results_count,
721 if meta.results_count == 1 { "" } else { "s" },
722 time_str
723 ),
724 Style::default().fg(Color::DarkGray),
725 ),
726 ]));
727 }
728
729 lines.extend(wrap_with_prefix(&msg.content, area.width, Color::Yellow));
731
732 lines.push(Line::from(Span::styled(
733 "╰───────────────────────────────────────────",
734 Style::default().fg(Color::Yellow),
735 )));
736 }
737 MessageRole::AssistantAnswer => {
738 lines.push(Line::from(""));
740 lines.push(Line::from(Span::styled(
741 "╭─ Assistant (Answer) ──────────────────────",
742 Style::default()
743 .fg(Color::Blue)
744 .add_modifier(Modifier::BOLD),
745 )));
746
747 lines.extend(render_markdown_with_prefix(
749 &msg.content,
750 area.width,
751 Color::Blue,
752 ));
753
754 lines.push(Line::from(Span::styled(
755 "╰───────────────────────────────────────────",
756 Style::default().fg(Color::Blue),
757 )));
758 }
759 MessageRole::System => {
760 lines.push(Line::from(""));
762 lines.push(Line::from(Span::styled(
763 "╭─ System ──────────────────────────────────",
764 Style::default()
765 .fg(Color::Yellow)
766 .add_modifier(Modifier::BOLD),
767 )));
768
769 lines.extend(wrap_with_prefix(&msg.content, area.width, Color::Yellow));
771
772 lines.push(Line::from(Span::styled(
773 "╰───────────────────────────────────────────",
774 Style::default().fg(Color::Yellow),
775 )));
776 }
777 }
778 }
779
780 if self.waiting {
782 const SPINNER_CHARS: [&str; 10] = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
784 let spinner_char = SPINNER_CHARS[self.spinner_frame % SPINNER_CHARS.len()];
785
786 let status_text = self
788 .status_message
789 .clone()
790 .unwrap_or_else(|| "Working...".to_string());
791
792 lines.push(Line::from(""));
793 lines.push(Line::from(Span::styled(
794 "╭─ Processing ──────────────────────────────",
795 Style::default()
796 .fg(Color::Cyan)
797 .add_modifier(Modifier::BOLD),
798 )));
799 lines.push(Line::from(vec![
800 Span::styled("│ ", Style::default().fg(Color::Cyan)),
801 Span::styled(
802 spinner_char,
803 Style::default()
804 .fg(Color::Cyan)
805 .add_modifier(Modifier::BOLD),
806 ),
807 Span::styled(
808 format!(" {}", status_text),
809 Style::default().fg(Color::White),
810 ),
811 ]));
812 lines.push(Line::from(Span::styled(
813 "╰───────────────────────────────────────────",
814 Style::default().fg(Color::Cyan),
815 )));
816 }
817
818 for _ in 0..8 {
821 lines.push(Line::from(""));
822 }
823
824 let total_lines = lines.len();
828 let visible_height = area.height.saturating_sub(2) as usize; let scroll = if total_lines <= visible_height {
831 0 } else {
833 let max_scroll = total_lines.saturating_sub(visible_height);
835
836 self.scroll_offset = self.scroll_offset.min(max_scroll);
838
839 max_scroll.saturating_sub(self.scroll_offset) as u16
840 };
841
842 let paragraph = Paragraph::new(lines)
843 .block(
844 Block::default()
845 .borders(Borders::ALL)
846 .title(" Messages ")
847 .border_style(Style::default().fg(Color::DarkGray)),
848 )
849 .scroll((scroll, 0));
850
851 f.render_widget(paragraph, area);
852 }
853
854 fn render_input(&self, f: &mut Frame, area: Rect) {
855 let input_display = if self.input.is_empty() {
856 "Type your question here...".to_string()
857 } else {
858 self.input.clone()
859 };
860
861 let input_style = if self.input.is_empty() {
862 Style::default().fg(Color::DarkGray)
863 } else {
864 Style::default().fg(Color::White)
865 };
866
867 let shortcuts = " Enter: Send | Ctrl+C: Quit | Ctrl+L: /clear | Ctrl+K: /compact | Ctrl+U: Clear input ";
869
870 let paragraph = Paragraph::new(input_display)
871 .block(
872 Block::default()
873 .borders(Borders::ALL)
874 .title(vec![
875 Span::raw(" "),
876 Span::styled(
877 ">",
878 Style::default()
879 .fg(Color::Green)
880 .add_modifier(Modifier::BOLD),
881 ),
882 Span::raw(" Input "),
883 ])
884 .title_bottom(Line::from(Span::styled(
885 shortcuts,
886 Style::default().fg(Color::DarkGray),
887 )))
888 .border_style(Style::default().fg(if self.waiting {
889 Color::DarkGray
890 } else {
891 Color::Green
892 })),
893 )
894 .style(input_style);
895
896 f.render_widget(paragraph, area);
897
898 if !self.waiting && !self.input.is_empty() {
900 f.set_cursor_position((area.x + 1 + (self.cursor as u16), area.y + 1));
901 }
902 }
903
904 fn handle_key(&mut self, key: KeyEvent) -> Result<()> {
905 if key.modifiers.contains(KeyModifiers::CONTROL) {
907 match key.code {
908 KeyCode::Char('c') | KeyCode::Char('d') => {
909 self.should_quit = true;
911 return Ok(());
912 }
913 KeyCode::Char('l') => {
914 self.handle_slash_command("/clear")?;
916 return Ok(());
917 }
918 KeyCode::Char('k') => {
919 self.handle_slash_command("/compact")?;
921 return Ok(());
922 }
923 KeyCode::Char('u') => {
924 self.input.clear();
926 self.cursor = 0;
927 return Ok(());
928 }
929 _ => {}
930 }
931 }
932
933 if self.waiting {
935 return Ok(());
936 }
937
938 match key.code {
940 KeyCode::Enter => {
941 self.handle_enter()?;
942 }
943 KeyCode::Char(c) => {
944 self.input.insert(self.cursor, c);
945 self.cursor += 1;
946 }
947 KeyCode::Backspace if self.cursor > 0 => {
948 self.input.remove(self.cursor - 1);
949 self.cursor -= 1;
950 }
951 KeyCode::Delete if self.cursor < self.input.len() => {
952 self.input.remove(self.cursor);
953 }
954 KeyCode::Left => {
955 self.cursor = self.cursor.saturating_sub(1);
956 }
957 KeyCode::Right if self.cursor < self.input.len() => {
958 self.cursor += 1;
959 }
960 KeyCode::Home => {
961 self.cursor = 0;
962 }
963 KeyCode::End => {
964 self.cursor = self.input.len();
965 }
966 KeyCode::Up => {
967 self.scroll_offset = self.scroll_offset.saturating_add(1);
969 }
970 KeyCode::Down => {
971 self.scroll_offset = self.scroll_offset.saturating_sub(1);
973 }
974 KeyCode::PageUp => {
975 self.scroll_offset = self.scroll_offset.saturating_add(10);
977 }
978 KeyCode::PageDown => {
979 self.scroll_offset = self.scroll_offset.saturating_sub(10);
981 }
982 _ => {}
983 }
984
985 Ok(())
986 }
987
988 fn handle_mouse(&mut self, mouse: MouseEvent) {
989 match mouse.kind {
991 MouseEventKind::ScrollUp => {
992 self.scroll_offset = self.scroll_offset.saturating_add(3);
994 }
995 MouseEventKind::ScrollDown => {
996 self.scroll_offset = self.scroll_offset.saturating_sub(3);
998 }
999 _ => {}
1000 }
1001 }
1002
1003 fn handle_enter(&mut self) -> Result<()> {
1004 let input = self.input.trim().to_string();
1005
1006 if input.is_empty() {
1007 return Ok(());
1008 }
1009
1010 if input.starts_with('/') {
1012 return self.handle_slash_command(&input);
1013 }
1014
1015 self.session.add_user_message(input.clone());
1017
1018 self.input.clear();
1020 self.cursor = 0;
1021
1022 self.scroll_offset = 0;
1024
1025 self.execute_query(&input)?;
1029
1030 Ok(())
1031 }
1032
1033 fn execute_query(&mut self, question: &str) -> Result<()> {
1034 self.waiting = true;
1035 self.status_message = Some("Analyzing question...".to_string());
1036
1037 let (tx, rx) = mpsc::channel();
1039 self.progress_rx = Some(rx);
1040
1041 let question = question.to_string();
1043 let cache_path = self.cache.path().to_path_buf();
1044 let provider_name = self.provider_name.clone();
1045 let model_override = self.model_override.clone();
1046
1047 let conversation_history = self.session.build_context();
1049
1050 std::thread::spawn(move || {
1052 let runtime = match tokio::runtime::Runtime::new() {
1054 Ok(rt) => rt,
1055 Err(e) => {
1056 let _ = tx.send(PhaseUpdate::Error {
1057 error: format!("Failed to create async runtime: {}", e),
1058 });
1059 return;
1060 }
1061 };
1062
1063 runtime.block_on(async {
1064 execute_query_async(
1065 &question,
1066 &conversation_history,
1067 cache_path,
1068 &provider_name,
1069 model_override.as_deref(),
1070 tx,
1071 )
1072 .await
1073 });
1074 });
1075
1076 Ok(())
1077 }
1078
1079 fn handle_progress_update(&mut self, update: PhaseUpdate) {
1080 match update {
1081 PhaseUpdate::Triaging => {
1082 self.status_message = Some("Analyzing question...".to_string());
1083 }
1084 PhaseUpdate::AnsweringFromContext => {
1085 self.status_message = Some("Answering from conversation...".to_string());
1086 }
1087 PhaseUpdate::Thinking {
1088 reasoning,
1089 needs_context,
1090 } => {
1091 self.status_message = Some("Thinking...".to_string());
1092 self.session.add_thinking_message(reasoning, needs_context);
1093 self.scroll_offset = 0; }
1095 PhaseUpdate::Tools {
1096 content,
1097 tool_calls,
1098 } => {
1099 self.status_message =
1100 Some(format!("Gathering context ({} tools)...", tool_calls.len()));
1101 self.session.add_tools_message(content, tool_calls);
1102 self.scroll_offset = 0;
1103 }
1104 PhaseUpdate::Queries { queries } => {
1105 self.status_message = Some(format!("Generated {} queries...", queries.len()));
1106 self.session.add_queries_message(queries);
1107 self.scroll_offset = 0;
1108 }
1109 PhaseUpdate::Executing {
1110 results_count,
1111 execution_time_ms,
1112 } => {
1113 self.status_message = Some(format!(
1114 "Found {} result{}...",
1115 results_count,
1116 if results_count == 1 { "" } else { "s" }
1117 ));
1118 self.session
1119 .add_execution_message(results_count, execution_time_ms);
1120 self.scroll_offset = 0;
1121 }
1122 PhaseUpdate::Reindexing {
1123 current,
1124 total,
1125 message,
1126 } => {
1127 let percentage = if total > 0 {
1128 (current as f32 / total as f32 * 100.0) as u8
1129 } else {
1130 0
1131 };
1132 self.status_message = Some(format!(
1133 "Reindexing cache: [{}/{}] {}% - {}",
1134 current, total, percentage, message
1135 ));
1136 }
1138 PhaseUpdate::Answer { answer } => {
1139 self.status_message = Some("Generating answer...".to_string());
1140 self.session.add_answer_message(answer);
1141 self.scroll_offset = 0;
1142 }
1143 PhaseUpdate::Error { error } => {
1144 self.session.add_system_message(format!("Error: {}", error));
1145 self.waiting = false;
1146 self.status_message = Some(format!("❌ Error: {}", error));
1147 self.progress_rx = None;
1148 self.scroll_offset = 0;
1149 }
1150 PhaseUpdate::Notice { message } => {
1151 self.status_message = Some(message);
1152 }
1153 PhaseUpdate::Done => {
1154 self.waiting = false;
1155 self.status_message = None;
1156 self.progress_rx = None;
1157 }
1158 }
1159 }
1160
1161 fn handle_slash_command(&mut self, command: &str) -> Result<()> {
1162 let command = command.trim();
1163
1164 match command {
1165 "/clear" => {
1166 self.session.clear();
1167 self.status_message = Some("✓ Conversation cleared".to_string());
1168 self.input.clear();
1169 self.cursor = 0;
1170
1171 self.session
1173 .add_system_message("Conversation cleared. Start fresh!".to_string());
1174 }
1175 "/compact" => {
1176 self.handle_compact()?;
1177 self.input.clear();
1178 self.cursor = 0;
1179 }
1180 "/help" => {
1181 self.session.add_system_message(
1182 "Available slash commands:\n\
1183 \n\
1184 • /clear - Clear conversation history\n\
1185 • /compact - Summarize old messages to save tokens\n\
1186 • /model [provider] [model] - Show or change provider/model\n\
1187 • /help - Show this help message\n\
1188 \n\
1189 Keyboard shortcuts:\n\
1190 • Enter - Send message\n\
1191 • Ctrl+C - Quit\n\
1192 • Ctrl+L - Clear conversation\n\
1193 • Ctrl+K - Compact conversation\n\
1194 • Ctrl+U - Clear input\n\
1195 • Up/Down - Scroll messages\n\
1196 • PgUp/PgDn - Fast scroll"
1197 .to_string(),
1198 );
1199 self.input.clear();
1200 self.cursor = 0;
1201 }
1202 _ if command.starts_with("/model") => {
1203 self.handle_model_command(command)?;
1204 self.input.clear();
1205 self.cursor = 0;
1206 }
1207 _ => {
1208 self.status_message = Some(format!("Unknown command: {}", command));
1209 }
1210 }
1211
1212 Ok(())
1213 }
1214
1215 fn handle_compact(&mut self) -> Result<()> {
1216 let (old_messages, removed_count, tokens_saved_potential) =
1218 self.session.prepare_compaction(4);
1219
1220 if old_messages.is_empty() {
1221 self.status_message = Some("Nothing to compact (less than 4 messages)".to_string());
1222 return Ok(());
1223 }
1224
1225 self.waiting = true;
1226 self.status_message = Some("Compacting conversation...".to_string());
1227
1228 let runtime = tokio::runtime::Runtime::new().context("Failed to create async runtime")?;
1230
1231 let provider_instance = {
1233 let mut config = super::config::load_config(self.cache.path())?;
1234 config.provider = self.provider_name.clone();
1235 let api_key = super::config::get_api_key(&config.provider)?;
1236 let model = super::config::resolve_model(&config, self.model_override.as_deref());
1237 super::providers::create_provider(
1238 &config.provider,
1239 api_key,
1240 model,
1241 super::config::get_provider_options(&config.provider),
1242 config.timeout_seconds,
1243 )?
1244 };
1245
1246 let prompt = format!(
1248 "Summarize the following conversation history concisely while retaining \
1249 key technical details, code findings, and decisions made. \
1250 Provide a 2-3 paragraph summary.\n\n{}",
1251 old_messages
1252 );
1253
1254 let summary =
1256 runtime.block_on(async { provider_instance.complete(&prompt, false).await })?;
1257
1258 self.session
1260 .apply_compaction(removed_count, summary.clone());
1261
1262 self.waiting = false;
1263 self.status_message = Some(format!(
1264 "✓ Compacted {} messages (saved ~{} tokens)",
1265 removed_count, tokens_saved_potential
1266 ));
1267
1268 Ok(())
1269 }
1270
1271 fn handle_model_command(&mut self, command: &str) -> Result<()> {
1272 let parts: Vec<&str> = command.split_whitespace().collect();
1273
1274 if parts.len() == 1 {
1276 self.session.add_system_message(format!(
1277 "Current configuration:\n\
1278 • Provider: {}\n\
1279 • Model: {}\n\
1280 \n\
1281 Available providers: openai, anthropic, openrouter\n\
1282 \n\
1283 Usage:\n\
1284 • /model <provider> - Switch provider (uses configured model or default)\n\
1285 • /model <provider> <model> - Switch to specific provider and model",
1286 self.session.provider(),
1287 self.session.model()
1288 ));
1289 return Ok(());
1290 }
1291
1292 let new_provider = parts[1].to_lowercase();
1294 let new_model_arg = parts.get(2).map(|s| s.to_string());
1295
1296 let valid_providers = ["openai", "anthropic", "openrouter", "openai-compatible"];
1298 if !valid_providers.contains(&new_provider.as_str()) {
1299 self.status_message = Some(format!(
1300 "Invalid provider '{}'. Available: {}",
1301 new_provider,
1302 valid_providers.join(", ")
1303 ));
1304 return Ok(());
1305 }
1306
1307 let new_model =
1309 super::config::resolve_model_for(&new_provider, None, new_model_arg.as_deref())
1310 .unwrap_or_else(|| super::providers::default_model_for(&new_provider).to_string());
1311
1312 if new_model.is_empty() {
1315 self.status_message = Some(format!(
1316 "/model {} requires a model name (e.g. /model {} <model>) — \
1317 self-hosted endpoints have no default. Run 'rfx llm config' first.",
1318 new_provider, new_provider
1319 ));
1320 return Ok(());
1321 }
1322
1323 self.session
1325 .update_provider(new_provider.clone(), new_model.clone());
1326 self.provider_name = new_provider.clone();
1327 self.model_override = new_model_arg.clone();
1328
1329 if let Err(e) = super::save_user_provider(&new_provider, Some(&new_model)) {
1331 log::warn!("Failed to save provider preference to config: {}", e);
1332 self.status_message = Some("⚠ Model changed but not saved to config".to_string());
1333 } else {
1334 self.status_message = Some(format!("✓ Switched to {} ({})", new_provider, new_model));
1335 }
1336
1337 self.session.add_system_message(format!(
1339 "Switched to provider '{}' with model '{}'.\n\
1340 \n\
1341 This preference has been saved to ~/.reflex/config.toml.",
1342 new_provider, new_model
1343 ));
1344
1345 Ok(())
1346 }
1347}
1348
1349async fn retry_with_backoff<F, Fut, T>(
1351 mut operation: F,
1352 max_retries: usize,
1353 operation_name: &str,
1354) -> Result<T>
1355where
1356 F: FnMut() -> Fut,
1357 Fut: std::future::Future<Output = Result<T>>,
1358{
1359 let mut last_error = None;
1360
1361 for attempt in 0..=max_retries {
1362 match operation().await {
1363 Ok(result) => return Ok(result),
1364 Err(e) => {
1365 let err_msg = e.to_string();
1366
1367 let wait_ms = if err_msg.contains("Rate limit exceeded") || err_msg.contains("429")
1369 {
1370 5000 * (attempt as u64 + 1)
1372 } else if err_msg.contains("timeout") || err_msg.contains("Timeout") {
1373 2000 * (attempt as u64 + 1)
1375 } else {
1376 1000 * (attempt as u64 + 1)
1378 };
1379
1380 if attempt < max_retries {
1381 log::warn!(
1382 "{} failed (attempt {}/{}): {}. Retrying in {}ms...",
1383 operation_name,
1384 attempt + 1,
1385 max_retries + 1,
1386 err_msg,
1387 wait_ms
1388 );
1389 tokio::time::sleep(tokio::time::Duration::from_millis(wait_ms)).await;
1390 }
1391
1392 last_error = Some(e);
1393 }
1394 }
1395 }
1396
1397 Err(last_error.unwrap())
1398}
1399
1400async fn triage_question(
1402 question: &str,
1403 conversation_history: &str,
1404 provider_name: &str,
1405 model_override: Option<&str>,
1406 cache_path: &std::path::Path,
1407) -> Result<TriageDecision> {
1408 let provider_instance = {
1410 let mut config = super::config::load_config(cache_path)?;
1411 config.provider = provider_name.to_string();
1412 let api_key = super::config::get_api_key(&config.provider)?;
1413 let model = super::config::resolve_model(&config, model_override);
1414 super::providers::create_provider(
1415 &config.provider,
1416 api_key,
1417 model,
1418 super::config::get_provider_options(&config.provider),
1419 config.timeout_seconds,
1420 )?
1421 };
1422
1423 let triage_prompt = format!(
1425 "You are a helpful coding assistant with access to a codebase search engine.\n\
1426 \n\
1427 {}\n\
1428 \n\
1429 USER'S NEW QUESTION: {}\n\
1430 \n\
1431 TASK: Determine if you can answer this question using ONLY the conversation history above, \
1432 or if you need to search the codebase.\n\
1433 \n\
1434 Answer \"direct\" if:\n\
1435 - It's a follow-up question about something already discussed\n\
1436 - It's asking for clarification or explanation of prior context\n\
1437 - It's a general programming question not specific to this codebase\n\
1438 - Examples: \"What does that mean?\", \"Can you explain X?\", \"Why?\"\n\
1439 \n\
1440 Answer \"search\" if:\n\
1441 - It's asking about code not yet discussed\n\
1442 - It requires finding specific files, functions, or patterns\n\
1443 - It's a new topic requiring codebase investigation\n\
1444 - Examples: \"How is auth implemented?\", \"Find all uses of X\", \"Where is Y defined?\"\n\
1445 \n\
1446 Respond with ONLY a single word: either \"direct\" or \"search\"",
1447 conversation_history, question
1448 );
1449
1450 let response = provider_instance.complete(&triage_prompt, false).await?;
1452 let decision = response.trim().to_lowercase();
1453
1454 if decision.contains("direct") {
1455 Ok(TriageDecision::DirectAnswer)
1456 } else {
1457 Ok(TriageDecision::NeedsSearch {
1458 reasoning: "Question requires codebase search".to_string(),
1459 })
1460 }
1461}
1462
1463async fn execute_query_async(
1465 question: &str,
1466 conversation_history: &str,
1467 cache_path: std::path::PathBuf,
1468 provider_name: &str,
1469 model_override: Option<&str>,
1470 tx: Sender<PhaseUpdate>,
1471) {
1472 let root_dir = cache_path.parent().unwrap_or(&cache_path);
1475 let cache = CacheManager::new(root_dir);
1476
1477 let codebase_context_str = super::context::CodebaseContext::extract(&cache)
1479 .ok()
1480 .map(|ctx| ctx.to_prompt_string());
1481
1482 let _ = tx.send(PhaseUpdate::Triaging);
1484
1485 let decision = match triage_question(
1486 question,
1487 conversation_history,
1488 provider_name,
1489 model_override,
1490 &cache_path,
1491 )
1492 .await
1493 {
1494 Ok(decision) => decision,
1495 Err(e) => {
1496 let msg = format!("LLM unavailable, falling back to search: {}", e);
1497 log::warn!("{}", msg);
1498 let _ = tx.send(PhaseUpdate::Notice { message: msg });
1499 TriageDecision::NeedsSearch {
1500 reasoning: "Triage failed, using search as fallback".to_string(),
1501 }
1502 }
1503 };
1504
1505 match decision {
1506 TriageDecision::DirectAnswer => {
1507 let _ = tx.send(PhaseUpdate::AnsweringFromContext);
1509
1510 let provider_instance = match (|| -> Result<_> {
1512 let mut config = super::config::load_config(&cache_path)?;
1513 config.provider = provider_name.to_string();
1514 let api_key = super::config::get_api_key(&config.provider)?;
1515 let model = super::config::resolve_model(&config, model_override);
1516 super::providers::create_provider(
1517 &config.provider,
1518 api_key,
1519 model,
1520 super::config::get_provider_options(&config.provider),
1521 config.timeout_seconds,
1522 )
1523 })() {
1524 Ok(provider) => provider,
1525 Err(e) => {
1526 let _ = tx.send(PhaseUpdate::Error {
1527 error: format!("Failed to create provider: {}", e),
1528 });
1529 return;
1530 }
1531 };
1532
1533 let answer_prompt = format!(
1534 "{}\n\nUSER'S QUESTION: {}\n\n\
1535 Answer the question based on the conversation history above. \
1536 Be concise and helpful.",
1537 conversation_history, question
1538 );
1539
1540 let answer_result = retry_with_backoff(
1542 || async { provider_instance.complete(&answer_prompt, false).await },
1543 2, "Answer generation",
1545 )
1546 .await;
1547
1548 match answer_result {
1549 Ok(answer) => {
1550 let _ = tx.send(PhaseUpdate::Answer { answer });
1551 let _ = tx.send(PhaseUpdate::Done);
1552 }
1553 Err(e) => {
1554 log::warn!(
1556 "Direct answer failed after retries, falling back to search: {}",
1557 e
1558 );
1559 let _ = tx.send(PhaseUpdate::Thinking {
1560 reasoning: format!(
1561 "Direct answer failed ({}), searching codebase as fallback",
1562 e
1563 ),
1564 needs_context: true,
1565 });
1566
1567 let agentic_config = AgenticConfig {
1569 max_iterations: 2,
1570 max_tools_per_phase: 5,
1571 enable_evaluation: true,
1572 eval_config: Default::default(),
1573 provider_override: Some(provider_name.to_string()),
1574 model_override: model_override.map(|s| s.to_string()),
1575 show_reasoning: false,
1576 verbose: false,
1577 debug: false,
1578 };
1579
1580 let reporter = Box::new(super::QuietReporter);
1581
1582 match super::run_agentic_loop(question, &cache, agentic_config, &*reporter)
1583 .await
1584 {
1585 Ok(agentic_response) => {
1586 if let Some(ref tools) = agentic_response.tools_executed
1588 && !tools.is_empty()
1589 {
1590 let content =
1591 format!("Gathered context using {} tools", tools.len());
1592 let _ = tx.send(PhaseUpdate::Tools {
1593 content,
1594 tool_calls: tools.clone(),
1595 });
1596 }
1597
1598 let results_count = agentic_response.total_count.unwrap_or(0);
1600
1601 if !agentic_response.queries.is_empty() {
1603 let query_strings: Vec<String> = agentic_response
1604 .queries
1605 .iter()
1606 .map(|q| q.command.clone())
1607 .collect();
1608
1609 let _ = tx.send(PhaseUpdate::Queries {
1610 queries: query_strings,
1611 });
1612
1613 let _ = tx.send(PhaseUpdate::Executing {
1614 results_count,
1615 execution_time_ms: 0,
1616 });
1617 }
1618
1619 let provider_instance = match (|| -> Result<_> {
1620 let mut config = super::config::load_config(&cache_path)?;
1621 config.provider = provider_name.to_string();
1622 let api_key = super::config::get_api_key(&config.provider)?;
1623 let model = super::config::resolve_model(&config, model_override);
1624 super::providers::create_provider(
1625 &config.provider,
1626 api_key,
1627 model,
1628 super::config::get_provider_options(&config.provider),
1629 config.timeout_seconds,
1630 )
1631 })() {
1632 Ok(provider) => provider,
1633 Err(e) => {
1634 let _ = tx.send(PhaseUpdate::Error {
1635 error: format!(
1636 "Failed to create provider for fallback: {}",
1637 e
1638 ),
1639 });
1640 return;
1641 }
1642 };
1643
1644 match super::generate_answer(
1645 question,
1646 &agentic_response.results,
1647 results_count,
1648 agentic_response.gathered_context.as_deref(),
1649 codebase_context_str.as_deref(),
1650 &*provider_instance,
1651 )
1652 .await
1653 {
1654 Ok(answer) => {
1655 let _ = tx.send(PhaseUpdate::Answer { answer });
1656 let _ = tx.send(PhaseUpdate::Done);
1657 }
1658 Err(e) => {
1659 let _ = tx.send(PhaseUpdate::Error {
1660 error: format!("Fallback search failed: {}", e),
1661 });
1662 }
1663 }
1664 }
1665 Err(e) => {
1666 let _ = tx.send(PhaseUpdate::Error {
1667 error: format!("Both direct answer and search failed: {}", e),
1668 });
1669 }
1670 }
1671 }
1672 }
1673 }
1674
1675 TriageDecision::NeedsSearch { reasoning } => {
1676 let _ = tx.send(PhaseUpdate::Thinking {
1678 reasoning,
1679 needs_context: true,
1680 });
1681
1682 let agentic_config = AgenticConfig {
1684 max_iterations: 2,
1685 max_tools_per_phase: 5,
1686 enable_evaluation: true,
1687 eval_config: Default::default(),
1688 provider_override: Some(provider_name.to_string()),
1689 model_override: model_override.map(|s| s.to_string()),
1690 show_reasoning: false,
1691 verbose: false,
1692 debug: false,
1693 };
1694
1695 let reporter = Box::new(super::QuietReporter);
1697
1698 let agentic_response =
1700 match super::run_agentic_loop(question, &cache, agentic_config, &*reporter).await {
1701 Ok(response) => response,
1702 Err(e) => {
1703 let _ = tx.send(PhaseUpdate::Error {
1704 error: format!("Agentic loop failed: {}", e),
1705 });
1706 return;
1707 }
1708 };
1709
1710 if let Some(ref tools) = agentic_response.tools_executed
1712 && !tools.is_empty()
1713 {
1714 let content = format!("Gathered context using {} tools", tools.len());
1715 let _ = tx.send(PhaseUpdate::Tools {
1716 content,
1717 tool_calls: tools.clone(),
1718 });
1719 }
1720
1721 let results_count = agentic_response.total_count.unwrap_or(0);
1723
1724 if !agentic_response.queries.is_empty() {
1726 let query_strings: Vec<String> = agentic_response
1727 .queries
1728 .iter()
1729 .map(|q| q.command.clone())
1730 .collect();
1731
1732 let _ = tx.send(PhaseUpdate::Queries {
1733 queries: query_strings,
1734 });
1735
1736 let start_time = std::time::Instant::now();
1738 let execution_time_ms = start_time.elapsed().as_millis() as u64;
1739
1740 let _ = tx.send(PhaseUpdate::Executing {
1741 results_count,
1742 execution_time_ms,
1743 });
1744 }
1745
1746 let provider_instance = match (|| -> Result<_> {
1748 let mut config = super::config::load_config(&cache_path)?;
1749 config.provider = provider_name.to_string();
1750 let api_key = super::config::get_api_key(&config.provider)?;
1751 let model = super::config::resolve_model(&config, model_override);
1752 super::providers::create_provider(
1753 &config.provider,
1754 api_key,
1755 model,
1756 super::config::get_provider_options(&config.provider),
1757 config.timeout_seconds,
1758 )
1759 })() {
1760 Ok(provider) => provider,
1761 Err(e) => {
1762 let _ = tx.send(PhaseUpdate::Error {
1763 error: format!("Failed to create provider: {}", e),
1764 });
1765 return;
1766 }
1767 };
1768
1769 let answer = match super::generate_answer(
1770 question,
1771 &agentic_response.results,
1772 results_count,
1773 agentic_response.gathered_context.as_deref(),
1774 codebase_context_str.as_deref(),
1775 &*provider_instance,
1776 )
1777 .await
1778 {
1779 Ok(answer) => answer,
1780 Err(e) => {
1781 let _ = tx.send(PhaseUpdate::Error {
1782 error: format!("Failed to generate answer: {}", e),
1783 });
1784 return;
1785 }
1786 };
1787
1788 let _ = tx.send(PhaseUpdate::Answer { answer });
1790
1791 let _ = tx.send(PhaseUpdate::Done);
1793 }
1794 }
1795}
1796
1797fn setup_terminal() -> Result<Terminal<CrosstermBackend<io::Stdout>>> {
1799 crossterm::terminal::enable_raw_mode()?;
1800 let mut stdout = io::stdout();
1801 crossterm::execute!(
1802 stdout,
1803 crossterm::terminal::EnterAlternateScreen,
1804 crossterm::event::EnableMouseCapture,
1805 crossterm::cursor::Show
1806 )?;
1807 let backend = CrosstermBackend::new(stdout);
1808 let terminal = Terminal::new(backend)?;
1809 Ok(terminal)
1810}
1811
1812fn restore_terminal(mut terminal: Terminal<CrosstermBackend<io::Stdout>>) -> Result<()> {
1814 crossterm::terminal::disable_raw_mode()?;
1815 crossterm::execute!(
1816 terminal.backend_mut(),
1817 crossterm::terminal::LeaveAlternateScreen,
1818 crossterm::event::DisableMouseCapture
1819 )?;
1820 terminal.show_cursor()?;
1821 Ok(())
1822}
1823
1824pub fn run_chat_mode(
1826 cache: CacheManager,
1827 provider: Option<String>,
1828 model: Option<String>,
1829) -> Result<()> {
1830 let provider_name = if let Some(p) = provider {
1832 p
1833 } else {
1834 let config = super::config::load_config(cache.path())?;
1836 config.provider
1837 };
1838
1839 let mut app = ChatApp::new(cache, provider_name, model)?;
1840 app.run()
1841}