1use std::collections::{HashMap, HashSet};
12use std::io;
13use std::sync::Arc;
14use std::time::Instant;
15
16use crossterm::{
17 event::{
18 self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEvent, KeyEventKind,
19 KeyModifiers, MouseEventKind,
20 },
21 terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
22 ExecutableCommand,
23};
24use ratatui::{
25 layout::Rect,
26 prelude::CrosstermBackend,
27 style::{Color, Style},
28 text::{Line, Span},
29 widgets::{Block, Borders, Paragraph, Wrap},
30 Terminal,
31};
32use throbber_widgets_tui::{Throbber, ThrobberState, BRAILLE_EIGHT_DOUBLE};
33use tokio::runtime::Handle;
34use tokio::sync::mpsc;
35
36use crate::agent::{FromControllerRx, LLMRegistry, ToControllerTx, UiMessage};
37use crate::controller::{
38 ControlCmd, ControllerInputPayload, LLMController, PermissionRegistry, PermissionResponse,
39 ToolResultStatus, TurnId, UserInteractionRegistry,
40};
41
42use super::layout::{LayoutContext, LayoutTemplate, WidgetSizes};
43use super::themes::{render_theme_picker, ThemePickerState};
44use super::commands::{
45 filter_commands, generate_help_message, get_default_commands, is_slash_command, parse_command,
46 SlashCommand,
47};
48use super::keys::{AppKeyAction, AppKeyResult, DefaultKeyHandler, ExitHandler, KeyBindings, KeyContext, KeyHandler};
49use super::messages::{different_random_index, random_message_index, FUNNY_MESSAGES};
50use super::widgets::{
51 widget_ids, ChatView, TextInput, ToolStatus, SessionInfo, SessionPickerState,
52 SlashPopupState, Widget, WidgetAction, WidgetKeyResult, render_session_picker, render_slash_popup,
53 PermissionPanel, QuestionPanel, welcome_art_styled,
54};
55use super::{app_theme, current_theme_name, default_theme_name, get_theme, init_theme};
56
57const PROMPT: &str = " \u{203A} ";
58const CONTINUATION_INDENT: &str = " ";
59
60const PENDING_STATUS_TOOLS: &str = "running tools...";
62const PENDING_STATUS_LLM: &str = "Processing response from LLM...";
63
64#[derive(Debug, Clone)]
66pub struct AppConfig {
67 pub agent_name: String,
69 pub version: String,
71 pub welcome_art: Vec<String>,
73 pub welcome_subtitle_indices: Vec<usize>,
75 pub custom_commands: Vec<SlashCommand>,
77}
78
79impl Default for AppConfig {
80 fn default() -> Self {
81 Self {
82 agent_name: "Agent".to_string(),
83 version: "0.1.0".to_string(),
84 welcome_art: vec![
85 String::new(),
86 " Type a message to start chatting...".to_string(),
87 ],
88 welcome_subtitle_indices: vec![1],
89 custom_commands: Vec::new(),
90 }
91 }
92}
93
94fn format_elapsed(duration: std::time::Duration) -> String {
96 let secs = duration.as_secs();
97 if secs < 60 {
98 format!("{}s", secs)
99 } else if secs < 3600 {
100 let mins = secs / 60;
101 let remaining_secs = secs % 60;
102 if remaining_secs == 0 {
103 format!("{}m", mins)
104 } else {
105 format!("{}m {}s", mins, remaining_secs)
106 }
107 } else {
108 let hours = secs / 3600;
109 let remaining_mins = (secs % 3600) / 60;
110 if remaining_mins == 0 {
111 format!("{}h", hours)
112 } else {
113 format!("{}h {}m", hours, remaining_mins)
114 }
115 }
116}
117
118fn format_tokens(tokens: i64) -> String {
120 if tokens >= 100_000 {
121 format!("{}K", tokens / 1000)
122 } else if tokens >= 1000 {
123 format!("{:.1}K", tokens as f64 / 1000.0)
124 } else {
125 format!("{}", tokens)
126 }
127}
128
129
130pub struct App {
131 config: AppConfig,
133
134 commands: Vec<SlashCommand>,
136
137 pub should_quit: bool,
138
139 to_controller: Option<ToControllerTx>,
141
142 from_controller: Option<FromControllerRx>,
144
145 controller: Option<Arc<LLMController>>,
147
148 llm_registry: Option<LLMRegistry>,
150
151 runtime_handle: Option<Handle>,
153
154 session_id: i64,
156
157 user_turn_counter: i64,
159
160 model_name: String,
162
163 context_used: i64,
165
166 context_limit: i32,
168
169 throbber_state: ThrobberState,
171
172 pub waiting_for_response: bool,
174
175 waiting_started: Option<Instant>,
177
178 animation_frame_counter: u8,
180
181 message_index: usize,
183
184 last_message_change: Option<Instant>,
186
187 current_turn_id: Option<TurnId>,
189
190 executing_tools: HashSet<String>,
192
193 pub widgets: HashMap<&'static str, Box<dyn Widget>>,
195
196 pub widget_priority_order: Vec<&'static str>,
198
199 pub filtered_commands: Vec<&'static SlashCommand>,
201
202 sessions: Vec<SessionInfo>,
204
205 session_chat_views: HashMap<i64, ChatView>,
207
208 custom_throbber_message: Option<String>,
210
211 user_interaction_registry: Option<Arc<UserInteractionRegistry>>,
213
214 permission_registry: Option<Arc<PermissionRegistry>>,
216
217 layout_template: LayoutTemplate,
219
220 key_handler: Box<dyn KeyHandler>,
222
223 exit_handler: Option<Box<dyn ExitHandler>>,
225}
226
227impl App {
228 pub fn new() -> Self {
229 Self::with_config(AppConfig::default())
230 }
231
232 pub fn with_config(config: AppConfig) -> Self {
233 let theme_name = default_theme_name();
235 if let Some(theme) = get_theme(theme_name) {
236 init_theme(theme_name, theme);
237 }
238
239 let mut commands: Vec<SlashCommand> = get_default_commands().to_vec();
241 commands.extend(config.custom_commands.clone());
242
243 Self {
244 config,
245 commands,
246 should_quit: false,
247 to_controller: None,
248 from_controller: None,
249 controller: None,
250 llm_registry: None,
251 runtime_handle: None,
252 session_id: 0,
253 user_turn_counter: 0,
254 model_name: "Not connected".to_string(),
255 context_used: 0,
256 context_limit: 0,
257 throbber_state: ThrobberState::default(),
258 waiting_for_response: false,
259 waiting_started: None,
260 animation_frame_counter: 0,
261 message_index: random_message_index(),
262 last_message_change: None,
263 current_turn_id: None,
264 executing_tools: HashSet::new(),
265 widgets: HashMap::new(),
266 widget_priority_order: Vec::new(),
267 filtered_commands: Vec::new(),
268 sessions: Vec::new(),
269 session_chat_views: HashMap::new(),
270 custom_throbber_message: None,
271 user_interaction_registry: None,
272 permission_registry: None,
273 layout_template: LayoutTemplate::default(),
274 key_handler: Box::new(DefaultKeyHandler::default()),
275 exit_handler: None,
276 }
277 }
278
279 pub fn register_widget<W: Widget>(&mut self, widget: W) {
284 let id = widget.id();
285 self.widgets.insert(id, Box::new(widget));
286 self.rebuild_priority_order();
287 }
288
289 pub fn rebuild_priority_order(&mut self) {
291 let mut order: Vec<_> = self.widgets.keys().copied().collect();
292 order.sort_by(|a, b| {
293 let priority_a = self.widgets.get(a).map(|w| w.priority()).unwrap_or(0);
294 let priority_b = self.widgets.get(b).map(|w| w.priority()).unwrap_or(0);
295 priority_b.cmp(&priority_a) });
297 self.widget_priority_order = order;
298 }
299
300 pub fn widget<W: Widget + 'static>(&self, id: &str) -> Option<&W> {
302 self.widgets.get(id).and_then(|w| w.as_any().downcast_ref::<W>())
303 }
304
305 pub fn widget_mut<W: Widget + 'static>(&mut self, id: &str) -> Option<&mut W> {
307 self.widgets.get_mut(id).and_then(|w| w.as_any_mut().downcast_mut::<W>())
308 }
309
310 pub fn has_widget(&self, id: &str) -> bool {
312 self.widgets.contains_key(id)
313 }
314
315 fn any_widget_blocks_input(&self) -> bool {
317 self.widgets.values().any(|w| w.is_active() && w.blocks_input())
318 }
319
320 fn chat(&self) -> Option<&ChatView> {
322 self.widget::<ChatView>(widget_ids::CHAT_VIEW)
323 }
324
325 fn chat_mut(&mut self) -> Option<&mut ChatView> {
327 self.widget_mut::<ChatView>(widget_ids::CHAT_VIEW)
328 }
329
330 fn input(&self) -> Option<&TextInput> {
332 self.widget::<TextInput>(widget_ids::TEXT_INPUT)
333 }
334
335 fn input_mut(&mut self) -> Option<&mut TextInput> {
337 self.widget_mut::<TextInput>(widget_ids::TEXT_INPUT)
338 }
339
340 fn is_chat_streaming(&self) -> bool {
342 self.chat().map(|c| c.is_streaming()).unwrap_or(false)
343 }
344
345 fn create_chat_view(&self) -> ChatView {
347 let welcome_lines: Vec<&str> = self.config.welcome_art.iter().map(|s| s.as_str()).collect();
349 let empty_state = welcome_art_styled(&welcome_lines, &self.config.welcome_subtitle_indices);
350
351 ChatView::new()
352 .with_title(&self.config.agent_name)
353 .with_empty_state(empty_state)
354 }
355
356 pub fn agent_name(&self) -> &str {
358 &self.config.agent_name
359 }
360
361 pub fn version(&self) -> &str {
363 &self.config.version
364 }
365
366 pub fn set_to_controller(&mut self, tx: ToControllerTx) {
368 self.to_controller = Some(tx);
369 }
370
371 pub fn set_from_controller(&mut self, rx: FromControllerRx) {
373 self.from_controller = Some(rx);
374 }
375
376 pub fn set_controller(&mut self, controller: Arc<LLMController>) {
378 self.controller = Some(controller);
379 }
380
381 pub fn set_llm_registry(&mut self, registry: LLMRegistry) {
383 self.llm_registry = Some(registry);
384 }
385
386 pub fn set_runtime_handle(&mut self, handle: Handle) {
388 self.runtime_handle = Some(handle);
389 }
390
391 pub fn set_user_interaction_registry(&mut self, registry: Arc<UserInteractionRegistry>) {
393 self.user_interaction_registry = Some(registry);
394 }
395
396 pub fn set_permission_registry(&mut self, registry: Arc<PermissionRegistry>) {
398 self.permission_registry = Some(registry);
399 }
400
401 pub fn set_session_id(&mut self, id: i64) {
403 self.session_id = id;
404 }
405
406 pub fn set_model_name(&mut self, name: impl Into<String>) {
408 self.model_name = name.into();
409 }
410
411 pub fn set_context_limit(&mut self, limit: i32) {
413 self.context_limit = limit;
414 }
415
416 pub fn set_layout(&mut self, template: LayoutTemplate) {
418 self.layout_template = template;
419 }
420
421 pub fn set_key_handler<H: KeyHandler>(&mut self, handler: H) {
426 self.key_handler = Box::new(handler);
427 }
428
429 pub fn set_key_handler_boxed(&mut self, handler: Box<dyn KeyHandler>) {
433 self.key_handler = handler;
434 }
435
436 pub fn set_key_bindings(&mut self, bindings: KeyBindings) {
441 self.key_handler = Box::new(DefaultKeyHandler::new(bindings));
442 }
443
444 pub fn set_exit_handler<H: ExitHandler>(&mut self, handler: H) {
449 self.exit_handler = Some(Box::new(handler));
450 }
451
452 pub fn set_exit_handler_boxed(&mut self, handler: Box<dyn ExitHandler>) {
454 self.exit_handler = Some(handler);
455 }
456
457 fn compute_widget_sizes(&self, frame_height: u16) -> WidgetSizes {
459 let mut heights = HashMap::new();
460 let mut is_active = HashMap::new();
461
462 for (id, widget) in &self.widgets {
463 heights.insert(*id, widget.required_height(frame_height));
464 is_active.insert(*id, widget.is_active());
465 }
466
467 WidgetSizes { heights, is_active }
468 }
469
470 fn build_layout_context<'a>(
472 &self,
473 frame_area: Rect,
474 show_throbber: bool,
475 prompt_len: usize,
476 indent_len: usize,
477 theme: &'a super::themes::Theme,
478 ) -> LayoutContext<'a> {
479 let frame_width = frame_area.width as usize;
480 let input_visual_lines = self
481 .input()
482 .map(|i| i.visual_line_count(frame_width, prompt_len, indent_len))
483 .unwrap_or(1);
484
485 let mut active_widgets = HashSet::new();
486 for (id, widget) in &self.widgets {
487 if widget.is_active() {
488 active_widgets.insert(*id);
489 }
490 }
491
492 LayoutContext {
493 frame_area,
494 show_throbber,
495 input_visual_lines,
496 theme,
497 active_widgets,
498 }
499 }
500
501 pub fn submit_message(&mut self) {
502 let content = match self.input_mut() {
504 Some(input) => input.take(),
505 None => return, };
507 if content.trim().is_empty() {
508 return;
509 }
510
511 if is_slash_command(&content) {
513 self.execute_command(&content);
514 return;
515 }
516
517 if let Some(chat) = self.chat_mut() {
519 chat.enable_auto_scroll();
520 chat.add_user_message(content.clone());
521 }
522
523 if self.session_id == 0 {
525 if let Some(chat) = self.chat_mut() {
526 chat.add_system_message(
527 "No active session. Use /new-session to create one.".to_string(),
528 );
529 }
530 return;
531 }
532
533 if let Some(ref tx) = self.to_controller {
535 self.user_turn_counter += 1;
536 let turn_id = TurnId::new_user_turn(self.user_turn_counter);
537 let payload = ControllerInputPayload::data(self.session_id, content, turn_id);
538
539 if tx.try_send(payload).is_err() {
541 if let Some(chat) = self.chat_mut() {
542 chat.add_system_message("Failed to send message to controller".to_string());
543 }
544 } else {
545 self.waiting_for_response = true;
547 self.waiting_started = Some(Instant::now());
548 self.message_index = random_message_index();
550 self.last_message_change = Some(Instant::now());
551 self.current_turn_id = Some(TurnId::new_user_turn(self.user_turn_counter));
553 }
554 }
555 }
556
557 pub fn interrupt_request(&mut self) {
559 if !self.waiting_for_response
561 && !self.is_chat_streaming()
562 && self.executing_tools.is_empty()
563 {
564 return;
565 }
566
567 if let Some(ref tx) = self.to_controller {
569 let payload = ControllerInputPayload::control(self.session_id, ControlCmd::Interrupt);
570 if tx.try_send(payload).is_ok() {
571 self.waiting_for_response = false;
573 self.waiting_started = None;
574 self.last_message_change = None;
575 self.executing_tools.clear();
576 if let Some(chat) = self.chat_mut() {
578 chat.complete_streaming();
579 }
580 self.current_turn_id = None;
582 if let Some(chat) = self.chat_mut() {
583 chat.add_system_message("Request cancelled".to_string());
584 }
585 }
586 }
587 }
588
589 fn execute_command(&mut self, input: &str) {
591 let Some((cmd_name, _args)) = parse_command(input) else {
592 if let Some(chat) = self.chat_mut() {
593 chat.add_system_message("Invalid command format".to_string());
594 }
595 return;
596 };
597
598 match cmd_name {
600 "themes" => {
601 self.cmd_themes();
602 return;
603 }
604 "sessions" => {
605 self.cmd_sessions();
606 return;
607 }
608 "clear" => {
609 self.cmd_clear();
610 return;
611 }
612 "compact" => {
613 self.cmd_compact();
614 return;
615 }
616 _ => {}
617 }
618
619 let result = match cmd_name {
620 "help" => self.cmd_help(),
621 "status" => self.cmd_status(),
622 "new-session" => self.cmd_new_session(),
623 "quit" => self.cmd_quit(),
624 "version" => self.cmd_version(),
625 _ => format!("Unknown command: /{}", cmd_name),
626 };
627
628 if let Some(chat) = self.chat_mut() {
629 chat.add_system_message(result);
630 }
631 }
632
633 fn cmd_help(&self) -> String {
634 generate_help_message(&self.commands)
635 }
636
637 fn cmd_clear(&mut self) {
638 let new_chat = self.create_chat_view();
640 self.widgets.insert(widget_ids::CHAT_VIEW, Box::new(new_chat));
641 self.rebuild_priority_order();
642 self.user_turn_counter = 0;
643
644 if self.session_id != 0 {
646 if let Some(ref tx) = self.to_controller {
647 let payload =
648 ControllerInputPayload::control(self.session_id, ControlCmd::Clear);
649 let _ = tx.try_send(payload);
650 }
651 }
652 }
653
654 fn cmd_compact(&mut self) {
655 if self.session_id == 0 {
657 if let Some(chat) = self.chat_mut() {
658 chat.add_system_message("No active session to compact".to_string());
659 }
660 return;
661 }
662
663 if let Some(ref tx) = self.to_controller {
665 let payload = ControllerInputPayload::control(self.session_id, ControlCmd::Compact);
666 if tx.try_send(payload).is_ok() {
667 self.waiting_for_response = true;
669 self.waiting_started = Some(Instant::now());
670 self.custom_throbber_message = Some("compacting...".to_string());
671 } else {
672 if let Some(chat) = self.chat_mut() {
673 chat.add_system_message("Failed to send compact command".to_string());
674 }
675 }
676 }
677 }
678
679 fn cmd_status(&self) -> String {
680 if self.session_id == 0 {
681 return "No active session".to_string();
682 }
683
684 format!(
685 "Session Status\n ID: {}\n Model: {}",
686 self.session_id, self.model_name
687 )
688 }
689
690 fn cmd_new_session(&mut self) -> String {
691 let Some(ref controller) = self.controller else {
692 return "Error: Controller not available".to_string();
693 };
694
695 let Some(ref handle) = self.runtime_handle else {
696 return "Error: Runtime not available".to_string();
697 };
698
699 let Some(ref registry) = self.llm_registry else {
700 return "Error: No LLM providers configured.\nSet ANTHROPIC_API_KEY or create config file".to_string();
701 };
702
703 let Some(config) = registry.get_default() else {
705 return "Error: No LLM providers configured.\nSet ANTHROPIC_API_KEY or create config file".to_string();
706 };
707
708 let model = config.model.clone();
709 let context_limit = config.context_limit;
710 let config = config.clone();
711
712 let controller = controller.clone();
714 let session_id = match handle.block_on(async { controller.create_session(config).await }) {
715 Ok(id) => id,
716 Err(e) => {
717 return format!("Error: Failed to create session: {}", e);
718 }
719 };
720
721 let session_info = SessionInfo::new(session_id, model.clone(), context_limit);
723 self.sessions.push(session_info);
724
725 if self.session_id != 0 {
727 if let Some(old_chat_box) = self.widgets.remove(widget_ids::CHAT_VIEW) {
729 if let Ok(old_chat) = old_chat_box.into_any().downcast::<ChatView>() {
730 self.session_chat_views.insert(self.session_id, *old_chat);
731 }
732 }
733 }
734
735 let new_chat = self.create_chat_view();
737 self.widgets.insert(widget_ids::CHAT_VIEW, Box::new(new_chat));
738 self.rebuild_priority_order();
739
740 self.session_id = session_id;
741 self.model_name = model.clone();
742 self.context_limit = context_limit;
743 self.context_used = 0;
744 self.user_turn_counter = 0;
745
746 String::new()
748 }
749
750 fn cmd_quit(&mut self) -> String {
751 self.should_quit = true;
752 "Goodbye!".to_string()
753 }
754
755 fn cmd_version(&self) -> String {
756 format!("{} v{}", self.config.agent_name, self.config.version)
757 }
758
759 fn cmd_themes(&mut self) {
760 if let Some(widget) = self.widgets.get_mut(widget_ids::THEME_PICKER) {
761 if let Some(picker) = widget.as_any_mut().downcast_mut::<ThemePickerState>() {
762 let current_name = current_theme_name();
763 let current_theme = app_theme();
764 picker.activate(¤t_name, current_theme);
765 }
766 }
767 }
768
769 fn cmd_sessions(&mut self) {
770 if let Some(session) = self.sessions.iter_mut().find(|s| s.id == self.session_id) {
772 session.context_used = self.context_used;
773 }
774
775 if let Some(widget) = self.widgets.get_mut(widget_ids::SESSION_PICKER) {
776 if let Some(picker) = widget.as_any_mut().downcast_mut::<SessionPickerState>() {
777 picker.activate(self.sessions.clone(), self.session_id);
778 }
779 }
780 }
781
782 pub fn switch_session(&mut self, session_id: i64) {
784 if session_id == self.session_id {
786 return;
787 }
788
789 if let Some(session) = self.sessions.iter_mut().find(|s| s.id == self.session_id) {
791 session.context_used = self.context_used;
792 }
793
794 if let Some(old_chat_box) = self.widgets.remove(widget_ids::CHAT_VIEW) {
796 if let Ok(old_chat) = old_chat_box.into_any().downcast::<ChatView>() {
797 self.session_chat_views.insert(self.session_id, *old_chat);
798 }
799 }
800
801 if let Some(session) = self.sessions.iter().find(|s| s.id == session_id) {
803 self.session_id = session_id;
804 self.model_name = session.model.clone();
805 self.context_used = session.context_used;
806 self.context_limit = session.context_limit;
807 self.user_turn_counter = 0;
808
809 let chat = if let Some(stored_chat) = self.session_chat_views.remove(&session_id) {
811 stored_chat
812 } else {
813 self.create_chat_view()
814 };
815 self.widgets.insert(widget_ids::CHAT_VIEW, Box::new(chat));
816 self.rebuild_priority_order();
817 }
818 }
819
820 pub fn add_session(&mut self, info: SessionInfo) {
822 self.sessions.push(info);
823 }
824
825 fn submit_question_panel_response(&mut self, tool_use_id: String, response: crate::controller::AskUserQuestionsResponse) {
827 if let (Some(registry), Some(handle)) =
829 (&self.user_interaction_registry, &self.runtime_handle)
830 {
831 let registry = registry.clone();
832 handle.spawn(async move {
833 if let Err(e) = registry.respond(&tool_use_id, response).await {
834 tracing::error!(%tool_use_id, ?e, "Failed to respond to interaction");
835 }
836 });
837 }
838
839 if let Some(widget) = self.widgets.get_mut(widget_ids::QUESTION_PANEL) {
841 if let Some(panel) = widget.as_any_mut().downcast_mut::<QuestionPanel>() {
842 panel.deactivate();
843 }
844 }
845 }
846
847 fn cancel_question_panel_response(&mut self, tool_use_id: String) {
849 if let (Some(registry), Some(handle)) =
851 (&self.user_interaction_registry, &self.runtime_handle)
852 {
853 let registry = registry.clone();
854 handle.spawn(async move {
855 if let Err(e) = registry.cancel(&tool_use_id).await {
856 tracing::warn!(%tool_use_id, ?e, "Failed to cancel interaction");
857 }
858 });
859 }
860
861 if let Some(widget) = self.widgets.get_mut(widget_ids::QUESTION_PANEL) {
863 if let Some(panel) = widget.as_any_mut().downcast_mut::<QuestionPanel>() {
864 panel.deactivate();
865 }
866 }
867 }
868
869 fn submit_permission_panel_response(&mut self, tool_use_id: String, response: PermissionResponse) {
871 if let (Some(registry), Some(handle)) = (&self.permission_registry, &self.runtime_handle) {
873 let registry = registry.clone();
874 handle.spawn(async move {
875 if let Err(e) = registry.respond(&tool_use_id, response).await {
876 tracing::error!(%tool_use_id, ?e, "Failed to respond to permission request");
877 }
878 });
879 }
880
881 if let Some(widget) = self.widgets.get_mut(widget_ids::PERMISSION_PANEL) {
883 if let Some(panel) = widget.as_any_mut().downcast_mut::<PermissionPanel>() {
884 panel.deactivate();
885 }
886 }
887 }
888
889 fn cancel_permission_panel_response(&mut self, tool_use_id: String) {
891 if let (Some(registry), Some(handle)) = (&self.permission_registry, &self.runtime_handle) {
893 let registry = registry.clone();
894 handle.spawn(async move {
895 if let Err(e) = registry.cancel(&tool_use_id).await {
896 tracing::warn!(%tool_use_id, ?e, "Failed to cancel permission request");
897 }
898 });
899 }
900
901 if let Some(widget) = self.widgets.get_mut(widget_ids::PERMISSION_PANEL) {
903 if let Some(panel) = widget.as_any_mut().downcast_mut::<PermissionPanel>() {
904 panel.deactivate();
905 }
906 }
907 }
908
909 fn process_controller_messages(&mut self) {
911 let mut messages = Vec::new();
913
914 if let Some(ref mut rx) = self.from_controller {
915 loop {
916 match rx.try_recv() {
917 Ok(msg) => messages.push(msg),
918 Err(mpsc::error::TryRecvError::Empty) => break,
919 Err(mpsc::error::TryRecvError::Disconnected) => {
920 tracing::warn!("Controller channel disconnected");
921 break;
922 }
923 }
924 }
925 }
926
927 for msg in messages {
929 self.handle_ui_message(msg);
930 }
931 }
932
933 fn handle_ui_message(&mut self, msg: UiMessage) {
935 match msg {
936 UiMessage::TextChunk { text, turn_id, .. } => {
937 if !self.is_current_turn(&turn_id) {
939 return;
940 }
941 if let Some(chat) = self.chat_mut() {
942 chat.append_streaming(&text);
943 }
944 }
945 UiMessage::Display { message, .. } => {
946 if let Some(chat) = self.chat_mut() {
947 chat.add_system_message(message);
948 }
949 }
950 UiMessage::Complete {
951 turn_id,
952 stop_reason,
953 ..
954 } => {
955 if !self.is_current_turn(&turn_id) {
957 return;
958 }
959
960 let is_tool_use = stop_reason.as_deref() == Some("tool_use");
962
963 if let Some(chat) = self.chat_mut() {
964 chat.complete_streaming();
965 }
966
967 if !is_tool_use {
969 self.waiting_for_response = false;
970 self.waiting_started = None;
971 self.last_message_change = None;
972 }
973 }
974 UiMessage::TokenUpdate {
975 input_tokens,
976 context_limit,
977 ..
978 } => {
979 self.context_used = input_tokens;
980 self.context_limit = context_limit;
981 }
982 UiMessage::Error { error, turn_id, .. } => {
983 if !self.is_current_turn(&turn_id) {
984 return;
985 }
986 if let Some(chat) = self.chat_mut() {
987 chat.complete_streaming();
988 }
989 self.waiting_for_response = false;
990 self.waiting_started = None;
991 self.last_message_change = None;
992 self.current_turn_id = None;
993 if let Some(chat) = self.chat_mut() {
994 chat.add_system_message(format!("Error: {}", error));
995 }
996 }
997 UiMessage::System { message, .. } => {
998 if let Some(chat) = self.chat_mut() {
999 chat.add_system_message(message);
1000 }
1001 }
1002 UiMessage::ToolExecuting {
1003 tool_use_id,
1004 display_name,
1005 display_title,
1006 ..
1007 } => {
1008 self.executing_tools.insert(tool_use_id.clone());
1009 if let Some(chat) = self.chat_mut() {
1010 chat.add_tool_message(&tool_use_id, &display_name, &display_title);
1011 }
1012 }
1013 UiMessage::ToolCompleted {
1014 tool_use_id,
1015 status,
1016 error,
1017 ..
1018 } => {
1019 self.executing_tools.remove(&tool_use_id);
1020 let tool_status = if status == ToolResultStatus::Success {
1021 ToolStatus::Completed
1022 } else {
1023 ToolStatus::Failed(error.unwrap_or_default())
1024 };
1025 if let Some(chat) = self.chat_mut() {
1026 chat.update_tool_status(&tool_use_id, tool_status);
1027 }
1028 }
1029 UiMessage::CommandComplete {
1030 command,
1031 success,
1032 message,
1033 ..
1034 } => {
1035 self.waiting_for_response = false;
1036 self.waiting_started = None;
1037 self.custom_throbber_message = None;
1038
1039 match command {
1040 ControlCmd::Compact => {
1041 if let Some(msg) = message {
1042 if let Some(chat) = self.chat_mut() {
1043 chat.add_system_message(msg);
1044 }
1045 }
1046 }
1047 ControlCmd::Clear => {}
1048 _ => {
1049 tracing::debug!(?command, ?success, "Command completed");
1050 }
1051 }
1052 }
1053 UiMessage::UserInteractionRequired {
1054 session_id,
1055 tool_use_id,
1056 request,
1057 turn_id,
1058 } => {
1059 if session_id == self.session_id {
1060 if let Some(chat) = self.chat_mut() {
1061 chat.update_tool_status(&tool_use_id, ToolStatus::WaitingForUser);
1062 }
1063 if let Some(widget) = self.widgets.get_mut(widget_ids::QUESTION_PANEL) {
1065 if let Some(panel) = widget.as_any_mut().downcast_mut::<QuestionPanel>() {
1066 panel.activate(tool_use_id, session_id, request, turn_id);
1067 }
1068 }
1069 }
1070 }
1071 UiMessage::PermissionRequired {
1072 session_id,
1073 tool_use_id,
1074 request,
1075 turn_id,
1076 } => {
1077 if session_id == self.session_id {
1078 if let Some(chat) = self.chat_mut() {
1079 chat.update_tool_status(&tool_use_id, ToolStatus::WaitingForUser);
1080 }
1081 if let Some(widget) = self.widgets.get_mut(widget_ids::PERMISSION_PANEL) {
1083 if let Some(panel) = widget.as_any_mut().downcast_mut::<PermissionPanel>() {
1084 panel.activate(tool_use_id, session_id, request, turn_id);
1085 }
1086 }
1087 }
1088 }
1089 }
1090 }
1091
1092 fn is_current_turn(&self, turn_id: &Option<TurnId>) -> bool {
1094 match (&self.current_turn_id, turn_id) {
1095 (Some(current), Some(incoming)) => current == incoming,
1096 (None, _) => false,
1097 (Some(_), None) => false,
1098 }
1099 }
1100
1101 pub fn scroll_up(&mut self) {
1102 if let Some(chat) = self.chat_mut() {
1103 chat.scroll_up();
1104 }
1105 }
1106
1107 pub fn scroll_down(&mut self) {
1108 if let Some(chat) = self.chat_mut() {
1109 chat.scroll_down();
1110 }
1111 }
1112
1113 fn format_context_display(&self) -> String {
1115 if self.context_limit == 0 {
1116 return String::new();
1117 }
1118
1119 let utilization = (self.context_used as f64 / self.context_limit as f64) * 100.0;
1120 let prefix = if utilization > 80.0 {
1121 "Context Low:"
1122 } else {
1123 "Context:"
1124 };
1125
1126 format!(
1127 "{} {}/{} ({:.0}%)",
1128 prefix,
1129 format_tokens(self.context_used),
1130 format_tokens(self.context_limit as i64),
1131 utilization
1132 )
1133 }
1134
1135 fn context_style(&self) -> Style {
1137 if self.context_limit == 0 {
1138 return app_theme().status_help;
1139 }
1140
1141 let utilization = (self.context_used as f64 / self.context_limit as f64) * 100.0;
1142 if utilization > 80.0 {
1143 Style::default().fg(Color::Yellow)
1144 } else {
1145 app_theme().status_help
1146 }
1147 }
1148
1149 fn handle_key(&mut self, key: KeyCode, modifiers: KeyModifiers) {
1151 let key_event = KeyEvent::new(key, modifiers);
1153 let context = KeyContext {
1154 input_empty: self.input().map(|i| i.is_empty()).unwrap_or(true),
1155 is_processing: self.waiting_for_response || self.is_chat_streaming(),
1156 widget_blocking: self.any_widget_blocks_input(),
1157 };
1158
1159 let result = self.key_handler.handle_key(key_event, &context);
1162
1163 match result {
1164 AppKeyResult::Handled => return,
1165 AppKeyResult::Action(action) => {
1166 self.execute_key_action(action);
1167 return;
1168 }
1169 AppKeyResult::NotHandled => {
1170 }
1172 }
1173
1174 let theme = app_theme();
1176
1177 let widget_ids_to_check: Vec<&'static str> = self.widget_priority_order.clone();
1179
1180 for widget_id in widget_ids_to_check {
1181 if let Some(widget) = self.widgets.get_mut(widget_id) {
1182 if widget.is_active() {
1183 match widget.handle_key(key_event, &theme) {
1184 WidgetKeyResult::Handled => return,
1185 WidgetKeyResult::Action(action) => {
1186 self.process_widget_action(action);
1187 return;
1188 }
1189 WidgetKeyResult::NotHandled => {
1190 }
1192 }
1193 }
1194 }
1195 }
1196
1197 if self.is_slash_popup_active() {
1199 self.handle_slash_popup_key(key);
1200 return;
1201 }
1202 }
1203
1204 fn execute_key_action(&mut self, action: AppKeyAction) {
1206 match action {
1207 AppKeyAction::MoveUp => {
1208 if let Some(input) = self.input_mut() {
1209 input.move_up();
1210 }
1211 }
1212 AppKeyAction::MoveDown => {
1213 if let Some(input) = self.input_mut() {
1214 input.move_down();
1215 }
1216 }
1217 AppKeyAction::MoveLeft => {
1218 if let Some(input) = self.input_mut() {
1219 input.move_left();
1220 }
1221 }
1222 AppKeyAction::MoveRight => {
1223 if let Some(input) = self.input_mut() {
1224 input.move_right();
1225 }
1226 }
1227 AppKeyAction::MoveLineStart => {
1228 if let Some(input) = self.input_mut() {
1229 input.move_to_line_start();
1230 }
1231 }
1232 AppKeyAction::MoveLineEnd => {
1233 if let Some(input) = self.input_mut() {
1234 input.move_to_line_end();
1235 }
1236 }
1237 AppKeyAction::DeleteCharBefore => {
1238 if let Some(input) = self.input_mut() {
1239 input.delete_char_before();
1240 }
1241 }
1242 AppKeyAction::DeleteCharAt => {
1243 if let Some(input) = self.input_mut() {
1244 input.delete_char_at();
1245 }
1246 }
1247 AppKeyAction::KillLine => {
1248 if let Some(input) = self.input_mut() {
1249 input.kill_line();
1250 }
1251 }
1252 AppKeyAction::InsertNewline => {
1253 if let Some(input) = self.input_mut() {
1254 input.insert_char('\n');
1255 }
1256 }
1257 AppKeyAction::InsertChar(c) => {
1258 if let Some(input) = self.input_mut() {
1259 input.insert_char(c);
1260 }
1261 if c == '/' && self.input().map(|i| i.buffer() == "/").unwrap_or(false) {
1263 self.activate_slash_popup();
1264 }
1265 }
1266 AppKeyAction::Submit => {
1267 self.submit_message();
1268 }
1269 AppKeyAction::Interrupt => {
1270 self.interrupt_request();
1271 }
1272 AppKeyAction::Quit => {
1273 self.should_quit = true;
1274 }
1275 AppKeyAction::RequestExit => {
1276 let should_quit = self.exit_handler
1278 .as_mut()
1279 .map(|h| h.on_exit())
1280 .unwrap_or(true);
1281 if should_quit {
1282 self.should_quit = true;
1283 }
1284 }
1285 AppKeyAction::ActivateSlashPopup => {
1286 self.activate_slash_popup();
1287 }
1288 }
1289 }
1290
1291 fn process_widget_action(&mut self, action: WidgetAction) {
1293 match action {
1294 WidgetAction::SubmitQuestion { tool_use_id, response } => {
1295 self.submit_question_panel_response(tool_use_id, response);
1296 }
1297 WidgetAction::CancelQuestion { tool_use_id } => {
1298 self.cancel_question_panel_response(tool_use_id);
1299 }
1300 WidgetAction::SubmitPermission { tool_use_id, response } => {
1301 self.submit_permission_panel_response(tool_use_id, response);
1302 }
1303 WidgetAction::CancelPermission { tool_use_id } => {
1304 self.cancel_permission_panel_response(tool_use_id);
1305 }
1306 WidgetAction::SwitchSession { session_id } => {
1307 self.switch_session(session_id);
1308 }
1309 WidgetAction::ExecuteCommand { command } => {
1310 if command.starts_with("__SLASH_INDEX_") {
1312 if let Ok(idx) = command.trim_start_matches("__SLASH_INDEX_").parse::<usize>() {
1313 self.execute_slash_command_at_index(idx);
1314 }
1315 } else {
1316 self.execute_command(&command);
1317 }
1318 }
1319 WidgetAction::Close => {
1320 }
1322 }
1323 }
1324
1325 fn is_slash_popup_active(&self) -> bool {
1327 self.widgets
1328 .get(widget_ids::SLASH_POPUP)
1329 .map(|w| w.is_active())
1330 .unwrap_or(false)
1331 }
1332
1333 fn activate_slash_popup(&mut self) {
1335 if let Some(widget) = self.widgets.get_mut(widget_ids::SLASH_POPUP) {
1336 if let Some(popup) = widget.as_any_mut().downcast_mut::<SlashPopupState>() {
1337 popup.activate();
1338 self.filtered_commands = filter_commands(get_default_commands(), "/");
1339 popup.set_filtered_count(self.filtered_commands.len());
1340 }
1341 }
1342 }
1343
1344 fn handle_slash_popup_key(&mut self, key: KeyCode) {
1346 match key {
1347 KeyCode::Up => {
1348 if let Some(widget) = self.widgets.get_mut(widget_ids::SLASH_POPUP) {
1349 if let Some(popup) = widget.as_any_mut().downcast_mut::<SlashPopupState>() {
1350 popup.select_previous();
1351 }
1352 }
1353 }
1354 KeyCode::Down => {
1355 if let Some(widget) = self.widgets.get_mut(widget_ids::SLASH_POPUP) {
1356 if let Some(popup) = widget.as_any_mut().downcast_mut::<SlashPopupState>() {
1357 popup.select_next();
1358 }
1359 }
1360 }
1361 KeyCode::Enter => {
1362 let selected_idx = self.widgets
1363 .get(widget_ids::SLASH_POPUP)
1364 .and_then(|w| w.as_any().downcast_ref::<SlashPopupState>())
1365 .map(|p| p.selected_index)
1366 .unwrap_or(0);
1367 self.execute_slash_command_at_index(selected_idx);
1368 }
1369 KeyCode::Esc => {
1370 if let Some(input) = self.input_mut() {
1371 input.clear();
1372 }
1373 if let Some(widget) = self.widgets.get_mut(widget_ids::SLASH_POPUP) {
1374 if let Some(popup) = widget.as_any_mut().downcast_mut::<SlashPopupState>() {
1375 popup.deactivate();
1376 }
1377 }
1378 self.filtered_commands.clear();
1379 }
1380 KeyCode::Backspace => {
1381 let is_just_slash = self.input().map(|i| i.buffer() == "/").unwrap_or(false);
1382 if is_just_slash {
1383 if let Some(input) = self.input_mut() {
1384 input.clear();
1385 }
1386 if let Some(widget) = self.widgets.get_mut(widget_ids::SLASH_POPUP) {
1387 if let Some(popup) = widget.as_any_mut().downcast_mut::<SlashPopupState>() {
1388 popup.deactivate();
1389 }
1390 }
1391 self.filtered_commands.clear();
1392 } else {
1393 if let Some(input) = self.input_mut() {
1394 input.delete_char_before();
1395 }
1396 let buffer = self.input().map(|i| i.buffer().to_string()).unwrap_or_default();
1397 self.filtered_commands = filter_commands(get_default_commands(), &buffer);
1398 if let Some(widget) = self.widgets.get_mut(widget_ids::SLASH_POPUP) {
1399 if let Some(popup) = widget.as_any_mut().downcast_mut::<SlashPopupState>() {
1400 popup.set_filtered_count(self.filtered_commands.len());
1401 }
1402 }
1403 }
1404 }
1405 KeyCode::Char(c) => {
1406 if let Some(input) = self.input_mut() {
1407 input.insert_char(c);
1408 }
1409 let buffer = self.input().map(|i| i.buffer().to_string()).unwrap_or_default();
1410 self.filtered_commands = filter_commands(get_default_commands(), &buffer);
1411 if let Some(widget) = self.widgets.get_mut(widget_ids::SLASH_POPUP) {
1412 if let Some(popup) = widget.as_any_mut().downcast_mut::<SlashPopupState>() {
1413 popup.set_filtered_count(self.filtered_commands.len());
1414 }
1415 }
1416 }
1417 _ => {
1418 if let Some(widget) = self.widgets.get_mut(widget_ids::SLASH_POPUP) {
1419 if let Some(popup) = widget.as_any_mut().downcast_mut::<SlashPopupState>() {
1420 popup.deactivate();
1421 }
1422 }
1423 }
1424 }
1425 }
1426
1427 fn execute_slash_command_at_index(&mut self, idx: usize) {
1429 if let Some(cmd) = self.filtered_commands.get(idx) {
1430 let cmd_name = cmd.name;
1431 if let Some(input) = self.input_mut() {
1432 input.clear();
1433 for c in format!("/{}", cmd_name).chars() {
1434 input.insert_char(c);
1435 }
1436 }
1437 if let Some(widget) = self.widgets.get_mut(widget_ids::SLASH_POPUP) {
1438 if let Some(popup) = widget.as_any_mut().downcast_mut::<SlashPopupState>() {
1439 popup.deactivate();
1440 }
1441 }
1442 self.filtered_commands.clear();
1443 self.submit_message();
1444 }
1445 }
1446
1447 pub fn run(&mut self) -> io::Result<()> {
1448 enable_raw_mode()?;
1449 io::stdout().execute(EnterAlternateScreen)?;
1450 io::stdout().execute(EnableMouseCapture)?;
1451
1452 let mut terminal = Terminal::new(CrosstermBackend::new(io::stdout()))?;
1453
1454 while !self.should_quit {
1455 self.process_controller_messages();
1456
1457 let show_throbber = self.waiting_for_response
1458 || self.is_chat_streaming()
1459 || !self.executing_tools.is_empty();
1460
1461 if show_throbber {
1463 self.animation_frame_counter = self.animation_frame_counter.wrapping_add(1);
1464 if self.animation_frame_counter % 6 == 0 {
1465 self.throbber_state.calc_next();
1466 if let Some(chat) = self.chat_mut() {
1467 chat.step_spinner();
1468 }
1469 }
1470
1471 if let Some(last_change) = self.last_message_change {
1473 let elapsed = last_change.elapsed().as_secs();
1474 let interval = 5 + (self.message_index % 11);
1475 if elapsed >= interval as u64 {
1476 self.message_index = different_random_index(self.message_index);
1477 self.last_message_change = Some(Instant::now());
1478 }
1479 }
1480 }
1481
1482 let prompt_len = PROMPT.chars().count();
1483 let indent_len = CONTINUATION_INDENT.len();
1484
1485 terminal.draw(|frame| {
1486 self.render_frame(frame, show_throbber, prompt_len, indent_len);
1487 })?;
1488
1489 let mut net_scroll: i32 = 0;
1491
1492 while event::poll(std::time::Duration::from_millis(0))? {
1493 match event::read()? {
1494 Event::Key(key) => {
1495 if key.kind == KeyEventKind::Press {
1496 self.handle_key(key.code, key.modifiers);
1497 }
1498 }
1499 Event::Mouse(mouse) => match mouse.kind {
1500 MouseEventKind::ScrollUp => net_scroll -= 1,
1501 MouseEventKind::ScrollDown => net_scroll += 1,
1502 _ => {}
1503 },
1504 _ => {}
1505 }
1506 }
1507
1508 if net_scroll < 0 {
1510 for _ in 0..(-net_scroll) {
1511 self.scroll_up();
1512 }
1513 } else if net_scroll > 0 {
1514 for _ in 0..net_scroll {
1515 self.scroll_down();
1516 }
1517 }
1518
1519 if net_scroll == 0 {
1520 std::thread::sleep(std::time::Duration::from_millis(16));
1521 }
1522 }
1523
1524 io::stdout().execute(DisableMouseCapture)?;
1525 disable_raw_mode()?;
1526 io::stdout().execute(LeaveAlternateScreen)?;
1527
1528 Ok(())
1529 }
1530
1531 fn render_frame(
1532 &mut self,
1533 frame: &mut ratatui::Frame,
1534 show_throbber: bool,
1535 prompt_len: usize,
1536 indent_len: usize,
1537 ) {
1538 let frame_area = frame.area();
1539 let frame_width = frame_area.width as usize;
1540 let frame_height = frame_area.height;
1541 let theme = app_theme();
1542
1543 let ctx = self.build_layout_context(frame_area, show_throbber, prompt_len, indent_len, &theme);
1545 let sizes = self.compute_widget_sizes(frame_height);
1546 let layout = self.layout_template.compute(&ctx, &sizes);
1547
1548 let theme_picker_active = sizes.is_active(widget_ids::THEME_PICKER);
1550 let session_picker_active = sizes.is_active(widget_ids::SESSION_PICKER);
1551 let question_panel_active = sizes.is_active(widget_ids::QUESTION_PANEL);
1552 let permission_panel_active = sizes.is_active(widget_ids::PERMISSION_PANEL);
1553
1554 for widget_id in &layout.render_order {
1556 if *widget_id == widget_ids::THEME_PICKER || *widget_id == widget_ids::SESSION_PICKER {
1558 continue;
1559 }
1560
1561 let Some(area) = layout.widget_areas.get(widget_id) else {
1563 continue;
1564 };
1565
1566 match *widget_id {
1568 id if id == widget_ids::CHAT_VIEW => {
1569 let pending_status: Option<&str> = if !self.executing_tools.is_empty() {
1570 Some(PENDING_STATUS_TOOLS)
1571 } else if self.waiting_for_response && !self.is_chat_streaming() {
1572 Some(PENDING_STATUS_LLM)
1573 } else {
1574 None
1575 };
1576 if let Some(chat) = self.chat_mut() {
1577 chat.render_chat(frame, *area, pending_status);
1578 }
1579 }
1580 id if id == widget_ids::TEXT_INPUT => {
1581 }
1583 id if id == widget_ids::SLASH_POPUP => {
1584 if let Some(widget) = self.widgets.get(widget_ids::SLASH_POPUP) {
1585 if let Some(popup_state) = widget.as_any().downcast_ref::<SlashPopupState>() {
1586 render_slash_popup(
1587 popup_state,
1588 &self.filtered_commands,
1589 frame,
1590 *area,
1591 &theme,
1592 );
1593 }
1594 }
1595 }
1596 _ => {
1597 if let Some(widget) = self.widgets.get(widget_id) {
1599 if widget.is_active() {
1600 widget.render(frame, *area, &theme);
1601 }
1602 }
1603 }
1604 }
1605 }
1606
1607 if let Some(input_area) = layout.input_area {
1609 if !question_panel_active && !permission_panel_active {
1610 if show_throbber {
1611 let message = self
1612 .custom_throbber_message
1613 .as_deref()
1614 .unwrap_or(FUNNY_MESSAGES[self.message_index]);
1615 let throbber = Throbber::default()
1616 .label(message)
1617 .style(theme.throbber_label)
1618 .throbber_style(theme.throbber_spinner)
1619 .throbber_set(BRAILLE_EIGHT_DOUBLE);
1620
1621 let throbber_block = Block::default()
1622 .borders(Borders::TOP | Borders::BOTTOM)
1623 .border_style(theme.input_border);
1624 let inner = throbber_block.inner(input_area);
1625 let throbber_inner = Rect::new(
1626 inner.x + 1,
1627 inner.y,
1628 inner.width.saturating_sub(1),
1629 inner.height,
1630 );
1631 frame.render_widget(throbber_block, input_area);
1632 frame.render_stateful_widget(throbber, throbber_inner, &mut self.throbber_state);
1633 } else if let Some(input) = self.input() {
1634 let input_lines: Vec<String> = input
1635 .buffer()
1636 .split('\n')
1637 .enumerate()
1638 .map(|(i, line)| {
1639 if i == 0 {
1640 format!("{}{}", PROMPT, line)
1641 } else {
1642 format!("{}{}", CONTINUATION_INDENT, line)
1643 }
1644 })
1645 .collect();
1646 let input_text = if input_lines.is_empty() {
1647 PROMPT.to_string()
1648 } else {
1649 input_lines.join("\n")
1650 };
1651
1652 let input_box = Paragraph::new(input_text)
1653 .block(
1654 Block::default()
1655 .borders(Borders::TOP | Borders::BOTTOM)
1656 .border_style(theme.input_border),
1657 )
1658 .wrap(Wrap { trim: false });
1659 frame.render_widget(input_box, input_area);
1660
1661 if !theme_picker_active && !session_picker_active {
1663 let (cursor_rel_x, cursor_rel_y) = input
1664 .cursor_display_position_wrapped(frame_width, prompt_len, indent_len);
1665 let cursor_x = input_area.x + cursor_rel_x;
1666 let cursor_y = input_area.y + 1 + cursor_rel_y;
1667 frame.set_cursor_position((cursor_x, cursor_y));
1668 }
1669 }
1670 }
1671 }
1672
1673 if let Some(status_area) = layout.status_bar_area {
1675 let cwd = std::env::current_dir()
1676 .map(|p| {
1677 let path_str = p.display().to_string();
1678 if let Some(home) = std::env::var_os("HOME") {
1679 let home_str = home.to_string_lossy();
1680 if path_str.starts_with(home_str.as_ref()) {
1681 return format!("~{}", &path_str[home_str.len()..]);
1682 }
1683 }
1684 path_str
1685 })
1686 .unwrap_or_else(|_| "unknown".to_string());
1687
1688 let help_text = if question_panel_active || permission_panel_active {
1689 String::new()
1690 } else if let Some(hint) = self.key_handler.status_hint() {
1691 format!(" {}", hint)
1692 } else if show_throbber {
1693 let elapsed = self
1694 .waiting_started
1695 .map(|start| start.elapsed())
1696 .unwrap_or_default();
1697 let elapsed_str = format_elapsed(elapsed);
1698 format!(" escape to interrupt ({})", elapsed_str)
1699 } else if self.session_id == 0 {
1700 " No session - type /new-session to start".to_string()
1701 } else if self.input().map(|i| i.is_empty()).unwrap_or(true) {
1702 " Ctrl-D to exit".to_string()
1703 } else {
1704 " Shift-Enter to add a new line".to_string()
1705 };
1706
1707 let context_str = self.format_context_display();
1708 let context_style = self.context_style();
1709
1710 let status_width = status_area.width as usize;
1711 let cwd_display = format!(" {}", cwd);
1712 let cwd_len = cwd_display.chars().count();
1713 let context_len = context_str.chars().count();
1714 let model_len = self.model_name.chars().count() + 1;
1715 let spacing = if context_len > 0 { 2 } else { 0 };
1716 let total_right = context_len + spacing + model_len;
1717 let line1_padding = status_width.saturating_sub(cwd_len + total_right);
1718
1719 let line1 = if context_len > 0 {
1720 Line::from(vec![
1721 Span::styled(&cwd_display, theme.status_help),
1722 Span::raw(" ".repeat(line1_padding)),
1723 Span::styled(&context_str, context_style),
1724 Span::raw(" "),
1725 Span::styled(format!("{} ", self.model_name), theme.status_model),
1726 ])
1727 } else {
1728 Line::from(vec![
1729 Span::styled(&cwd_display, theme.status_help),
1730 Span::raw(" ".repeat(line1_padding)),
1731 Span::styled(format!("{} ", self.model_name), theme.status_model),
1732 ])
1733 };
1734
1735 let line2 = Line::from(vec![Span::styled(&help_text, theme.status_help)]);
1736
1737 let status_msg = Paragraph::new(vec![line1, line2]);
1738 frame.render_widget(status_msg, status_area);
1739 }
1740
1741 if theme_picker_active {
1743 if let Some(widget) = self.widgets.get(widget_ids::THEME_PICKER) {
1744 if let Some(picker) = widget.as_any().downcast_ref::<ThemePickerState>() {
1745 render_theme_picker(picker, frame, frame_area);
1746 }
1747 }
1748 }
1749
1750 if session_picker_active {
1751 if let Some(widget) = self.widgets.get(widget_ids::SESSION_PICKER) {
1752 if let Some(picker) = widget.as_any().downcast_ref::<SessionPickerState>() {
1753 render_session_picker(picker, frame, frame_area, &theme);
1754 }
1755 }
1756 }
1757 }
1758}
1759
1760impl Default for App {
1761 fn default() -> Self {
1762 Self::new()
1763 }
1764}