1use std::collections::{HashMap, HashSet};
12use std::io;
13use std::sync::Arc;
14use std::time::Instant;
15
16use crossterm::{
17 ExecutableCommand,
18 event::{
19 self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEvent, KeyEventKind,
20 KeyModifiers, MouseEventKind,
21 },
22 terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
23};
24use ratatui::{
25 Terminal,
26 layout::Rect,
27 prelude::CrosstermBackend,
28 widgets::{Block, Borders, Paragraph, Wrap},
29};
30use throbber_widgets_tui::{BRAILLE_EIGHT_DOUBLE, Throbber, ThrobberState};
31use tokio::runtime::Handle;
32use tokio::sync::mpsc;
33
34use crate::agent::{FromControllerRx, LLMRegistry, ToControllerTx, UiMessage};
35use crate::controller::{
36 ControlCmd, ControllerInputPayload, LLMController, PermissionPanelResponse, PermissionRegistry,
37 ToolResultStatus, TurnId, UserInteractionRegistry,
38};
39
40use super::commands::{
41 CommandContext, CommandResult, PendingAction, SlashCommand, is_slash_command, parse_command,
42};
43use super::keys::{
44 AppKeyAction, AppKeyResult, DefaultKeyHandler, ExitHandler, KeyBindings, KeyContext,
45 KeyHandler, NavigationHelper,
46};
47use super::layout::{LayoutContext, LayoutTemplate, WidgetSizes};
48use super::themes::{ThemePickerState, render_theme_picker};
49use super::widgets::{
50 BatchPermissionPanel, ChatView, ConversationView, ConversationViewFactory, PermissionPanel,
51 QuestionPanel, SessionInfo, SessionPickerState, SlashPopupState, StatusBar, StatusBarData,
52 TextInput, ToolStatus, Widget, WidgetAction, WidgetKeyContext, WidgetKeyResult,
53 render_session_picker, render_slash_popup, widget_ids,
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
64pub type ProcessingMessageFn = Arc<dyn Fn() -> String + Send + Sync>;
67
68pub struct AppConfig {
70 pub agent_name: String,
72 pub version: String,
74 pub commands: Option<Vec<Box<dyn SlashCommand>>>,
76 pub command_extension: Option<Box<dyn std::any::Any + Send>>,
78 pub processing_message: String,
80 pub processing_message_fn: Option<ProcessingMessageFn>,
83 pub error_no_session: Option<String>,
85}
86
87impl std::fmt::Debug for AppConfig {
88 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89 f.debug_struct("AppConfig")
90 .field("agent_name", &self.agent_name)
91 .field("version", &self.version)
92 .field(
93 "commands",
94 &self
95 .commands
96 .as_ref()
97 .map(|c| format!("<{} commands>", c.len())),
98 )
99 .field(
100 "command_extension",
101 &self.command_extension.as_ref().map(|_| "<extension>"),
102 )
103 .field("processing_message", &self.processing_message)
104 .field(
105 "processing_message_fn",
106 &self.processing_message_fn.as_ref().map(|_| "<fn>"),
107 )
108 .field("error_no_session", &self.error_no_session)
109 .finish()
110 }
111}
112
113impl Default for AppConfig {
114 fn default() -> Self {
115 Self {
116 agent_name: "Agent".to_string(),
117 version: "0.1.0".to_string(),
118 commands: None, command_extension: None,
120 processing_message: "Processing request...".to_string(),
121 processing_message_fn: None,
122 error_no_session: None,
123 }
124 }
125}
126
127pub struct App {
131 agent_name: String,
133
134 version: String,
136
137 commands: Vec<Box<dyn SlashCommand>>,
139
140 command_extension: Option<Box<dyn std::any::Any + Send>>,
142
143 processing_message: String,
145
146 processing_message_fn: Option<ProcessingMessageFn>,
148
149 error_no_session: Option<String>,
151
152 pub should_quit: bool,
154
155 to_controller: Option<ToControllerTx>,
157
158 from_controller: Option<FromControllerRx>,
160
161 controller: Option<Arc<LLMController>>,
163
164 llm_registry: Option<LLMRegistry>,
166
167 runtime_handle: Option<Handle>,
169
170 session_id: i64,
172
173 user_turn_counter: i64,
175
176 model_name: String,
178
179 context_used: i64,
181
182 context_limit: i32,
184
185 throbber_state: ThrobberState,
187
188 pub waiting_for_response: bool,
190
191 waiting_started: Option<Instant>,
193
194 animation_frame_counter: u8,
196
197 current_turn_id: Option<TurnId>,
199
200 executing_tools: HashSet<String>,
202
203 pub widgets: HashMap<&'static str, Box<dyn Widget>>,
205
206 pub widget_priority_order: Vec<&'static str>,
208
209 filtered_command_indices: Vec<usize>,
211
212 sessions: Vec<SessionInfo>,
214
215 session_states: HashMap<i64, Box<dyn std::any::Any + Send>>,
217
218 conversation_view: Box<dyn ConversationView>,
220
221 conversation_factory: ConversationViewFactory,
223
224 custom_throbber_message: Option<String>,
226
227 user_interaction_registry: Option<Arc<UserInteractionRegistry>>,
229
230 permission_registry: Option<Arc<PermissionRegistry>>,
232
233 layout_template: LayoutTemplate,
235
236 key_handler: Box<dyn KeyHandler>,
238
239 exit_handler: Option<Box<dyn ExitHandler>>,
241}
242
243impl App {
244 pub fn new() -> Self {
246 Self::with_config(AppConfig::default())
247 }
248
249 pub fn with_config(config: AppConfig) -> Self {
251 use super::commands::default_commands;
252
253 let theme_name = default_theme_name();
255 if let Some(theme) = get_theme(theme_name) {
256 init_theme(theme_name, theme);
257 }
258
259 let commands = config.commands.unwrap_or_else(default_commands);
261
262 let default_factory: ConversationViewFactory = Box::new(|| Box::new(ChatView::new()));
264
265 let mut app = Self {
266 agent_name: config.agent_name,
267 version: config.version,
268 commands,
269 command_extension: config.command_extension,
270 processing_message: config.processing_message,
271 processing_message_fn: config.processing_message_fn,
272 error_no_session: config.error_no_session,
273 should_quit: false,
274 to_controller: None,
275 from_controller: None,
276 controller: None,
277 llm_registry: None,
278 runtime_handle: None,
279 session_id: 0,
280 user_turn_counter: 0,
281 model_name: "Not connected".to_string(),
282 context_used: 0,
283 context_limit: 0,
284 throbber_state: ThrobberState::default(),
285 waiting_for_response: false,
286 waiting_started: None,
287 animation_frame_counter: 0,
288 current_turn_id: None,
289 executing_tools: HashSet::new(),
290 widgets: HashMap::new(),
291 widget_priority_order: Vec::new(),
292 filtered_command_indices: Vec::new(),
293 sessions: Vec::new(),
294 session_states: HashMap::new(),
295 conversation_view: (default_factory)(),
296 conversation_factory: default_factory,
297 custom_throbber_message: None,
298 user_interaction_registry: None,
299 permission_registry: None,
300 layout_template: LayoutTemplate::default(),
301 key_handler: Box::new(DefaultKeyHandler::default()),
302 exit_handler: None,
303 };
304
305 app.register_widget(StatusBar::new());
307 app.register_widget(TextInput::new());
308
309 app
310 }
311
312 pub fn register_widget<W: Widget>(&mut self, widget: W) {
317 let id = widget.id();
318 self.widgets.insert(id, Box::new(widget));
319 self.rebuild_priority_order();
320 }
321
322 pub fn rebuild_priority_order(&mut self) {
324 let mut order: Vec<_> = self.widgets.keys().copied().collect();
325 order.sort_by(|a, b| {
326 let priority_a = self.widgets.get(a).map(|w| w.priority()).unwrap_or(0);
327 let priority_b = self.widgets.get(b).map(|w| w.priority()).unwrap_or(0);
328 priority_b.cmp(&priority_a) });
330 self.widget_priority_order = order;
331 }
332
333 pub fn widget<W: Widget + 'static>(&self, id: &str) -> Option<&W> {
335 self.widgets
336 .get(id)
337 .and_then(|w| w.as_any().downcast_ref::<W>())
338 }
339
340 pub fn widget_mut<W: Widget + 'static>(&mut self, id: &str) -> Option<&mut W> {
342 self.widgets
343 .get_mut(id)
344 .and_then(|w| w.as_any_mut().downcast_mut::<W>())
345 }
346
347 pub fn has_widget(&self, id: &str) -> bool {
349 self.widgets.contains_key(id)
350 }
351
352 fn any_widget_blocks_input(&self) -> bool {
354 self.widgets
355 .values()
356 .any(|w| w.is_active() && w.blocks_input())
357 }
358
359 pub fn set_conversation_factory<F>(&mut self, factory: F)
375 where
376 F: Fn() -> Box<dyn ConversationView> + Send + Sync + 'static,
377 {
378 self.conversation_factory = Box::new(factory);
379 self.conversation_view = (self.conversation_factory)();
381 }
382
383 fn input(&self) -> Option<&TextInput> {
385 self.widget::<TextInput>(widget_ids::TEXT_INPUT)
386 }
387
388 fn input_mut(&mut self) -> Option<&mut TextInput> {
390 self.widget_mut::<TextInput>(widget_ids::TEXT_INPUT)
391 }
392
393 fn is_chat_streaming(&self) -> bool {
395 self.conversation_view.is_streaming()
396 }
397
398 pub fn agent_name(&self) -> &str {
400 &self.agent_name
401 }
402
403 pub fn version(&self) -> &str {
405 &self.version
406 }
407
408 pub fn set_to_controller(&mut self, tx: ToControllerTx) {
410 self.to_controller = Some(tx);
411 }
412
413 pub fn set_from_controller(&mut self, rx: FromControllerRx) {
415 self.from_controller = Some(rx);
416 }
417
418 pub fn set_controller(&mut self, controller: Arc<LLMController>) {
420 self.controller = Some(controller);
421 }
422
423 pub fn set_llm_registry(&mut self, registry: LLMRegistry) {
425 self.llm_registry = Some(registry);
426 }
427
428 pub fn set_runtime_handle(&mut self, handle: Handle) {
430 self.runtime_handle = Some(handle);
431 }
432
433 pub fn set_user_interaction_registry(&mut self, registry: Arc<UserInteractionRegistry>) {
435 self.user_interaction_registry = Some(registry);
436 }
437
438 pub fn set_permission_registry(&mut self, registry: Arc<PermissionRegistry>) {
440 self.permission_registry = Some(registry);
441 }
442
443 pub fn set_session_id(&mut self, id: i64) {
445 self.session_id = id;
446 }
447
448 pub fn set_model_name(&mut self, name: impl Into<String>) {
450 self.model_name = name.into();
451 }
452
453 pub fn set_context_limit(&mut self, limit: i32) {
455 self.context_limit = limit;
456 }
457
458 pub fn set_layout(&mut self, template: LayoutTemplate) {
460 self.layout_template = template;
461 }
462
463 pub fn set_key_handler<H: KeyHandler>(&mut self, handler: H) {
468 self.key_handler = Box::new(handler);
469 }
470
471 pub fn set_key_handler_boxed(&mut self, handler: Box<dyn KeyHandler>) {
475 self.key_handler = handler;
476 }
477
478 pub fn set_key_bindings(&mut self, bindings: KeyBindings) {
483 self.key_handler = Box::new(DefaultKeyHandler::new(bindings));
484 }
485
486 pub fn set_exit_handler<H: ExitHandler>(&mut self, handler: H) {
491 self.exit_handler = Some(Box::new(handler));
492 }
493
494 pub fn set_exit_handler_boxed(&mut self, handler: Box<dyn ExitHandler>) {
496 self.exit_handler = Some(handler);
497 }
498
499 fn compute_widget_sizes(&self, frame_height: u16) -> WidgetSizes {
501 let mut heights = HashMap::new();
502 let mut is_active = HashMap::new();
503
504 for (id, widget) in &self.widgets {
505 heights.insert(*id, widget.required_height(frame_height));
506 is_active.insert(*id, widget.is_active());
507 }
508
509 WidgetSizes { heights, is_active }
510 }
511
512 fn build_layout_context<'a>(
514 &self,
515 frame_area: Rect,
516 show_throbber: bool,
517 prompt_len: usize,
518 indent_len: usize,
519 theme: &'a super::themes::Theme,
520 ) -> LayoutContext<'a> {
521 let frame_width = frame_area.width as usize;
522 let input_visual_lines = self
523 .input()
524 .map(|i| i.visual_line_count(frame_width, prompt_len, indent_len))
525 .unwrap_or(1);
526
527 let mut active_widgets = HashSet::new();
528 for (id, widget) in &self.widgets {
529 if widget.is_active() {
530 active_widgets.insert(*id);
531 }
532 }
533
534 LayoutContext {
535 frame_area,
536 show_throbber,
537 input_visual_lines,
538 theme,
539 active_widgets,
540 }
541 }
542
543 pub fn submit_message(&mut self) {
544 let content = match self.input_mut() {
546 Some(input) => input.take(),
547 None => return, };
549 if content.trim().is_empty() {
550 return;
551 }
552
553 if is_slash_command(&content) {
555 self.execute_command(&content);
556 return;
557 }
558
559 self.conversation_view.enable_auto_scroll();
561 self.conversation_view.add_user_message(content.clone());
562
563 if self.session_id == 0 {
565 let msg = self.error_no_session.clone().unwrap_or_else(|| {
566 "No active session. Use /new-session to create one.".to_string()
567 });
568 self.conversation_view.add_system_message(msg);
569 return;
570 }
571
572 if let Some(ref tx) = self.to_controller {
574 self.user_turn_counter += 1;
575 let turn_id = TurnId::new_user_turn(self.user_turn_counter);
576 let payload = ControllerInputPayload::data(self.session_id, content, turn_id);
577
578 if tx.try_send(payload).is_err() {
580 self.conversation_view
581 .add_system_message("Failed to send message to controller".to_string());
582 } else {
583 self.waiting_for_response = true;
585 self.waiting_started = Some(Instant::now());
586 self.current_turn_id = Some(TurnId::new_user_turn(self.user_turn_counter));
588 }
589 }
590 }
591
592 pub fn interrupt_request(&mut self) {
594 if !self.waiting_for_response
596 && !self.is_chat_streaming()
597 && self.executing_tools.is_empty()
598 {
599 return;
600 }
601
602 if let Some(ref tx) = self.to_controller {
604 let payload = ControllerInputPayload::control(self.session_id, ControlCmd::Interrupt);
605 if tx.try_send(payload).is_ok() {
606 self.waiting_for_response = false;
608 self.waiting_started = None;
609 self.executing_tools.clear();
610 self.conversation_view.complete_streaming();
612 self.current_turn_id = None;
614 self.conversation_view
615 .add_system_message("Request cancelled".to_string());
616 }
617 }
618 }
619
620 fn execute_command(&mut self, input: &str) {
622 let Some((cmd_name, args)) = parse_command(input) else {
623 self.conversation_view
624 .add_system_message("Invalid command format".to_string());
625 return;
626 };
627
628 let cmd_idx = self.commands.iter().position(|c| c.name() == cmd_name);
630 let Some(cmd_idx) = cmd_idx else {
631 self.conversation_view
632 .add_system_message(format!("Unknown command: /{}", cmd_name));
633 return;
634 };
635
636 let (result, pending_actions) = {
638 let extension = self.command_extension.take();
640 let extension_ref = extension.as_ref().map(|e| e.as_ref() as &dyn std::any::Any);
641
642 let mut ctx = CommandContext::new(
643 self.session_id,
644 &self.agent_name,
645 &self.version,
646 &self.commands,
647 &mut *self.conversation_view,
648 self.to_controller.as_ref(),
649 extension_ref,
650 );
651
652 let result = self.commands[cmd_idx].execute(args, &mut ctx);
654 let pending_actions = ctx.take_pending_actions();
655
656 self.command_extension = extension;
658
659 (result, pending_actions)
660 };
661
662 for action in pending_actions {
664 match action {
665 PendingAction::OpenThemePicker => self.cmd_themes(),
666 PendingAction::OpenSessionPicker => self.cmd_sessions(),
667 PendingAction::ClearConversation => self.cmd_clear(),
668 PendingAction::CompactConversation => self.cmd_compact(),
669 PendingAction::CreateNewSession => {
670 self.cmd_new_session();
671 }
672 PendingAction::Quit => {
673 self.should_quit = true;
674 }
675 }
676 }
677
678 match result {
680 CommandResult::Ok | CommandResult::Handled => {}
681 CommandResult::Message(msg) => {
682 self.conversation_view.add_system_message(msg);
683 }
684 CommandResult::Error(err) => {
685 self.conversation_view
686 .add_system_message(format!("Error: {}", err));
687 }
688 CommandResult::Quit => {
689 self.should_quit = true;
690 }
691 }
692 }
693
694 fn cmd_clear(&mut self) {
695 self.conversation_view = (self.conversation_factory)();
697 self.user_turn_counter = 0;
698
699 if self.session_id != 0
701 && let Some(ref tx) = self.to_controller
702 {
703 let payload = ControllerInputPayload::control(self.session_id, ControlCmd::Clear);
704 if let Err(e) = tx.try_send(payload) {
705 tracing::warn!("Failed to send clear command to controller: {}", e);
706 }
707 }
708 }
709
710 fn cmd_compact(&mut self) {
711 if self.session_id == 0 {
713 self.conversation_view
714 .add_system_message("No active session to compact".to_string());
715 return;
716 }
717
718 if let Some(ref tx) = self.to_controller {
720 let payload = ControllerInputPayload::control(self.session_id, ControlCmd::Compact);
721 if tx.try_send(payload).is_ok() {
722 self.waiting_for_response = true;
724 self.waiting_started = Some(Instant::now());
725 self.custom_throbber_message = Some("compacting...".to_string());
726 } else {
727 self.conversation_view
728 .add_system_message("Failed to send compact command".to_string());
729 }
730 }
731 }
732
733 fn cmd_new_session(&mut self) -> String {
734 let Some(ref controller) = self.controller else {
735 return "Error: Controller not available".to_string();
736 };
737
738 let Some(ref handle) = self.runtime_handle else {
739 return "Error: Runtime not available".to_string();
740 };
741
742 let Some(ref registry) = self.llm_registry else {
743 return "Error: No LLM providers configured.\nSet ANTHROPIC_API_KEY or create config file".to_string();
744 };
745
746 let Some(config) = registry.get_default() else {
748 return "Error: No LLM providers configured.\nSet ANTHROPIC_API_KEY or create config file".to_string();
749 };
750
751 let model = config.model.clone();
752 let context_limit = config.context_limit;
753 let config = config.clone();
754
755 let controller = controller.clone();
757 let session_id = match handle.block_on(async { controller.create_session(config).await }) {
758 Ok(id) => id,
759 Err(e) => {
760 return format!("Error: Failed to create session: {}", e);
761 }
762 };
763
764 let session_info = SessionInfo::new(session_id, model.clone(), context_limit);
766 self.sessions.push(session_info);
767
768 if self.session_id != 0 {
770 let state = self.conversation_view.save_state();
771 self.session_states.insert(self.session_id, state);
772 }
773
774 self.conversation_view = (self.conversation_factory)();
776
777 self.session_id = session_id;
778 self.model_name = model.clone();
779 self.context_limit = context_limit;
780 self.context_used = 0;
781 self.user_turn_counter = 0;
782
783 String::new()
785 }
786
787 fn cmd_themes(&mut self) {
788 if let Some(widget) = self.widgets.get_mut(widget_ids::THEME_PICKER)
789 && let Some(picker) = widget.as_any_mut().downcast_mut::<ThemePickerState>()
790 {
791 let current_name = current_theme_name();
792 let current_theme = app_theme();
793 picker.activate(¤t_name, current_theme);
794 }
795 }
796
797 fn cmd_sessions(&mut self) {
798 if let Some(session) = self.sessions.iter_mut().find(|s| s.id == self.session_id) {
800 session.context_used = self.context_used;
801 }
802
803 if let Some(widget) = self.widgets.get_mut(widget_ids::SESSION_PICKER)
804 && let Some(picker) = widget.as_any_mut().downcast_mut::<SessionPickerState>()
805 {
806 picker.activate(self.sessions.clone(), self.session_id);
807 }
808 }
809
810 pub fn switch_session(&mut self, session_id: i64) {
812 if session_id == self.session_id {
814 return;
815 }
816
817 if let Some(session) = self.sessions.iter_mut().find(|s| s.id == self.session_id) {
819 session.context_used = self.context_used;
820 }
821
822 let state = self.conversation_view.save_state();
824 self.session_states.insert(self.session_id, state);
825
826 if let Some(session) = self.sessions.iter().find(|s| s.id == session_id) {
828 self.session_id = session_id;
829 self.model_name = session.model.clone();
830 self.context_used = session.context_used;
831 self.context_limit = session.context_limit;
832 self.user_turn_counter = 0;
833
834 if let Some(stored_state) = self.session_states.remove(&session_id) {
836 self.conversation_view.restore_state(stored_state);
837 } else {
838 self.conversation_view = (self.conversation_factory)();
839 }
840 }
841 }
842
843 pub fn add_session(&mut self, info: SessionInfo) {
845 self.sessions.push(info);
846 }
847
848 fn submit_question_panel_response(
850 &mut self,
851 tool_use_id: String,
852 response: crate::controller::AskUserQuestionsResponse,
853 ) {
854 if let (Some(registry), Some(handle)) =
856 (&self.user_interaction_registry, &self.runtime_handle)
857 {
858 let registry = registry.clone();
859 handle.spawn(async move {
860 if let Err(e) = registry.respond(&tool_use_id, response).await {
861 tracing::error!(%tool_use_id, ?e, "Failed to respond to interaction");
862 }
863 });
864 }
865
866 if let Some(widget) = self.widgets.get_mut(widget_ids::QUESTION_PANEL)
868 && let Some(panel) = widget.as_any_mut().downcast_mut::<QuestionPanel>()
869 {
870 panel.deactivate();
871 }
872 }
873
874 fn cancel_question_panel_response(&mut self, tool_use_id: String) {
876 if let (Some(registry), Some(handle)) =
878 (&self.user_interaction_registry, &self.runtime_handle)
879 {
880 let registry = registry.clone();
881 handle.spawn(async move {
882 if let Err(e) = registry.cancel(&tool_use_id).await {
883 tracing::warn!(%tool_use_id, ?e, "Failed to cancel interaction");
884 }
885 });
886 }
887
888 if let Some(widget) = self.widgets.get_mut(widget_ids::QUESTION_PANEL)
890 && let Some(panel) = widget.as_any_mut().downcast_mut::<QuestionPanel>()
891 {
892 panel.deactivate();
893 }
894 }
895
896 fn submit_permission_panel_response(
898 &mut self,
899 tool_use_id: String,
900 response: PermissionPanelResponse,
901 ) {
902 if let (Some(registry), Some(handle)) = (&self.permission_registry, &self.runtime_handle) {
904 let registry = registry.clone();
905 handle.spawn(async move {
906 if let Err(e) = registry.respond_to_request(&tool_use_id, response).await {
907 tracing::error!(%tool_use_id, ?e, "Failed to respond to permission request");
908 }
909 });
910 }
911
912 if let Some(widget) = self.widgets.get_mut(widget_ids::PERMISSION_PANEL)
914 && let Some(panel) = widget.as_any_mut().downcast_mut::<PermissionPanel>()
915 {
916 panel.deactivate();
917 }
918 }
919
920 fn cancel_permission_panel_response(&mut self, tool_use_id: String) {
922 if let (Some(registry), Some(handle)) = (&self.permission_registry, &self.runtime_handle) {
924 let registry = registry.clone();
925 handle.spawn(async move {
926 if let Err(e) = registry.cancel(&tool_use_id).await {
927 tracing::warn!(%tool_use_id, ?e, "Failed to cancel permission request");
928 }
929 });
930 }
931
932 if let Some(widget) = self.widgets.get_mut(widget_ids::PERMISSION_PANEL)
934 && let Some(panel) = widget.as_any_mut().downcast_mut::<PermissionPanel>()
935 {
936 panel.deactivate();
937 }
938 }
939
940 fn submit_batch_permission_response(
942 &mut self,
943 batch_id: String,
944 response: crate::permissions::BatchPermissionResponse,
945 ) {
946 if let (Some(registry), Some(handle)) = (&self.permission_registry, &self.runtime_handle) {
948 let registry = registry.clone();
949 handle.spawn(async move {
950 if let Err(e) = registry.respond_to_batch(&batch_id, response).await {
951 tracing::error!(%batch_id, ?e, "Failed to respond to batch permission request");
952 }
953 });
954 }
955
956 if let Some(widget) = self.widgets.get_mut(widget_ids::BATCH_PERMISSION_PANEL)
958 && let Some(panel) = widget
959 .as_any_mut()
960 .downcast_mut::<crate::widgets::BatchPermissionPanel>()
961 {
962 panel.deactivate();
963 }
964 }
965
966 fn cancel_batch_permission_response(&mut self, batch_id: String) {
968 if let (Some(registry), Some(handle)) = (&self.permission_registry, &self.runtime_handle) {
970 let registry = registry.clone();
971 handle.spawn(async move {
972 if let Err(e) = registry.cancel_batch(&batch_id).await {
973 tracing::warn!(%batch_id, ?e, "Failed to cancel batch permission request");
974 }
975 });
976 }
977
978 if let Some(widget) = self.widgets.get_mut(widget_ids::BATCH_PERMISSION_PANEL)
980 && let Some(panel) = widget
981 .as_any_mut()
982 .downcast_mut::<crate::widgets::BatchPermissionPanel>()
983 {
984 panel.deactivate();
985 }
986 }
987
988 fn process_controller_messages(&mut self) {
990 let mut messages = Vec::new();
992
993 if let Some(ref mut rx) = self.from_controller {
994 loop {
995 match rx.try_recv() {
996 Ok(msg) => messages.push(msg),
997 Err(mpsc::error::TryRecvError::Empty) => break,
998 Err(mpsc::error::TryRecvError::Disconnected) => {
999 tracing::warn!("Controller channel disconnected");
1000 break;
1001 }
1002 }
1003 }
1004 }
1005
1006 for msg in messages {
1008 self.handle_ui_message(msg);
1009 }
1010 }
1011
1012 fn handle_ui_message(&mut self, msg: UiMessage) {
1014 match msg {
1015 UiMessage::TextChunk { text, turn_id, .. } => {
1016 if !self.is_current_turn(&turn_id) {
1018 return;
1019 }
1020 self.conversation_view.append_streaming(&text);
1021 }
1022 UiMessage::Display { message, .. } => {
1023 self.conversation_view.add_system_message(message);
1024 }
1025 UiMessage::Complete {
1026 turn_id,
1027 stop_reason,
1028 ..
1029 } => {
1030 if !self.is_current_turn(&turn_id) {
1032 return;
1033 }
1034
1035 let is_tool_use = stop_reason.as_deref() == Some("tool_use");
1037
1038 self.conversation_view.complete_streaming();
1039
1040 if !is_tool_use {
1042 self.waiting_for_response = false;
1043 self.waiting_started = None;
1044 }
1045 }
1046 UiMessage::TokenUpdate {
1047 input_tokens,
1048 context_limit,
1049 ..
1050 } => {
1051 self.context_used = input_tokens;
1052 self.context_limit = context_limit;
1053 }
1054 UiMessage::Error { error, turn_id, .. } => {
1055 let should_process = self.is_current_turn(&turn_id)
1058 || (self.current_turn_id.is_some() && turn_id.is_none());
1059 if !should_process {
1060 return;
1061 }
1062 self.conversation_view.complete_streaming();
1063 self.waiting_for_response = false;
1064 self.waiting_started = None;
1065 self.current_turn_id = None;
1066 self.conversation_view
1067 .add_system_message(format!("Error: {}", error));
1068 }
1069 UiMessage::System { message, .. } => {
1070 self.conversation_view.add_system_message(message);
1071 }
1072 UiMessage::ToolExecuting {
1073 tool_use_id,
1074 display_name,
1075 display_title,
1076 ..
1077 } => {
1078 self.executing_tools.insert(tool_use_id.clone());
1079 self.conversation_view.add_tool_message(
1080 &tool_use_id,
1081 &display_name,
1082 &display_title,
1083 );
1084 }
1085 UiMessage::ToolCompleted {
1086 tool_use_id,
1087 status,
1088 error,
1089 ..
1090 } => {
1091 self.executing_tools.remove(&tool_use_id);
1092 let tool_status = if status == ToolResultStatus::Success {
1093 ToolStatus::Completed
1094 } else {
1095 ToolStatus::Failed(error.unwrap_or_default())
1096 };
1097 self.conversation_view
1098 .update_tool_status(&tool_use_id, tool_status);
1099 }
1100 UiMessage::CommandComplete {
1101 command,
1102 success,
1103 message,
1104 ..
1105 } => {
1106 self.waiting_for_response = false;
1107 self.waiting_started = None;
1108 self.custom_throbber_message = None;
1109
1110 match command {
1111 ControlCmd::Compact => {
1112 if let Some(msg) = message {
1113 self.conversation_view.add_system_message(msg);
1114 }
1115 }
1116 ControlCmd::Clear => {}
1117 _ => {
1118 tracing::debug!(?command, ?success, "Command completed");
1119 }
1120 }
1121 }
1122 UiMessage::UserInteractionRequired {
1123 session_id,
1124 tool_use_id,
1125 request,
1126 turn_id,
1127 } => {
1128 if session_id == self.session_id {
1129 self.conversation_view
1130 .update_tool_status(&tool_use_id, ToolStatus::WaitingForUser);
1131 if let Some(widget) = self.widgets.get_mut(widget_ids::QUESTION_PANEL)
1133 && let Some(panel) = widget.as_any_mut().downcast_mut::<QuestionPanel>()
1134 {
1135 panel.activate(tool_use_id, session_id, request, turn_id);
1136 }
1137 }
1138 }
1139 UiMessage::PermissionRequired {
1140 session_id,
1141 tool_use_id,
1142 request,
1143 turn_id,
1144 } => {
1145 if session_id == self.session_id {
1146 self.conversation_view
1147 .update_tool_status(&tool_use_id, ToolStatus::WaitingForUser);
1148 if let Some(widget) = self.widgets.get_mut(widget_ids::PERMISSION_PANEL)
1150 && let Some(panel) = widget.as_any_mut().downcast_mut::<PermissionPanel>()
1151 {
1152 panel.activate(tool_use_id, session_id, request, turn_id);
1153 }
1154 }
1155 }
1156 UiMessage::BatchPermissionRequired {
1157 session_id,
1158 batch,
1159 turn_id,
1160 } => {
1161 if session_id == self.session_id {
1162 for request in &batch.requests {
1164 self.conversation_view
1165 .update_tool_status(&request.id, ToolStatus::WaitingForUser);
1166 }
1167 if let Some(widget) = self.widgets.get_mut(widget_ids::BATCH_PERMISSION_PANEL)
1169 && let Some(panel) =
1170 widget.as_any_mut().downcast_mut::<BatchPermissionPanel>()
1171 {
1172 panel.activate(session_id, batch, turn_id);
1173 }
1174 }
1175 }
1176 }
1177 }
1178
1179 fn is_current_turn(&self, turn_id: &Option<TurnId>) -> bool {
1181 match (&self.current_turn_id, turn_id) {
1182 (Some(current), Some(incoming)) => current == incoming,
1183 (None, _) => false,
1184 (Some(_), None) => false,
1185 }
1186 }
1187
1188 pub fn scroll_up(&mut self) {
1189 self.conversation_view.scroll_up();
1190 }
1191
1192 pub fn scroll_down(&mut self) {
1193 self.conversation_view.scroll_down();
1194 }
1195
1196 fn get_cwd(&self) -> String {
1198 std::env::current_dir()
1199 .map(|p| {
1200 let path_str = p.display().to_string();
1201 if let Some(home) = std::env::var_os("HOME") {
1202 let home_str = home.to_string_lossy();
1203 if path_str.starts_with(home_str.as_ref()) {
1204 return format!("~{}", &path_str[home_str.len()..]);
1205 }
1206 }
1207 path_str
1208 })
1209 .unwrap_or_else(|_| "unknown".to_string())
1210 }
1211
1212 fn handle_key(&mut self, key: KeyCode, modifiers: KeyModifiers) {
1214 let key_event = KeyEvent::new(key, modifiers);
1216 let context = KeyContext {
1217 input_empty: self.input().map(|i| i.is_empty()).unwrap_or(true),
1218 is_processing: self.waiting_for_response || self.is_chat_streaming(),
1219 widget_blocking: self.any_widget_blocks_input(),
1220 };
1221
1222 let result = self.key_handler.handle_key(key_event, &context);
1225
1226 match result {
1227 AppKeyResult::Handled => return,
1228 AppKeyResult::Action(action) => {
1229 self.execute_key_action(action);
1230 return;
1231 }
1232 AppKeyResult::NotHandled => {
1233 }
1235 }
1236
1237 let theme = app_theme();
1239 let nav = NavigationHelper::new(self.key_handler.bindings());
1240 let widget_ctx = WidgetKeyContext { theme: &theme, nav };
1241
1242 let widget_ids_to_check: Vec<&'static str> = self.widget_priority_order.clone();
1244
1245 for widget_id in widget_ids_to_check {
1246 if let Some(widget) = self.widgets.get_mut(widget_id)
1247 && widget.is_active()
1248 {
1249 match widget.handle_key(key_event, &widget_ctx) {
1250 WidgetKeyResult::Handled => return,
1251 WidgetKeyResult::Action(action) => {
1252 self.process_widget_action(action);
1253 return;
1254 }
1255 WidgetKeyResult::NotHandled => {
1256 }
1258 }
1259 }
1260 }
1261
1262 if self.is_slash_popup_active() {
1264 self.handle_slash_popup_key(key);
1265 }
1266 }
1267
1268 fn execute_key_action(&mut self, action: AppKeyAction) {
1270 match action {
1271 AppKeyAction::MoveUp => {
1272 if let Some(input) = self.input_mut() {
1273 input.move_up();
1274 }
1275 }
1276 AppKeyAction::MoveDown => {
1277 if let Some(input) = self.input_mut() {
1278 input.move_down();
1279 }
1280 }
1281 AppKeyAction::MoveLeft => {
1282 if let Some(input) = self.input_mut() {
1283 input.move_left();
1284 }
1285 }
1286 AppKeyAction::MoveRight => {
1287 if let Some(input) = self.input_mut() {
1288 input.move_right();
1289 }
1290 }
1291 AppKeyAction::MoveLineStart => {
1292 if let Some(input) = self.input_mut() {
1293 input.move_to_line_start();
1294 }
1295 }
1296 AppKeyAction::MoveLineEnd => {
1297 if let Some(input) = self.input_mut() {
1298 input.move_to_line_end();
1299 }
1300 }
1301 AppKeyAction::DeleteCharBefore => {
1302 if let Some(input) = self.input_mut() {
1303 input.delete_char_before();
1304 }
1305 }
1306 AppKeyAction::DeleteCharAt => {
1307 if let Some(input) = self.input_mut() {
1308 input.delete_char_at();
1309 }
1310 }
1311 AppKeyAction::KillLine => {
1312 if let Some(input) = self.input_mut() {
1313 input.kill_line();
1314 }
1315 }
1316 AppKeyAction::InsertNewline => {
1317 if let Some(input) = self.input_mut() {
1318 input.insert_char('\n');
1319 }
1320 }
1321 AppKeyAction::InsertChar(c) => {
1322 if let Some(input) = self.input_mut() {
1323 input.insert_char(c);
1324 }
1325 if c == '/' && self.input().map(|i| i.buffer() == "/").unwrap_or(false) {
1327 self.activate_slash_popup();
1328 }
1329 }
1330 AppKeyAction::Submit => {
1331 self.submit_message();
1332 }
1333 AppKeyAction::Interrupt => {
1334 self.interrupt_request();
1335 }
1336 AppKeyAction::Quit => {
1337 self.should_quit = true;
1338 }
1339 AppKeyAction::RequestExit => {
1340 let should_quit = self
1342 .exit_handler
1343 .as_mut()
1344 .map(|h| h.on_exit())
1345 .unwrap_or(true);
1346 if should_quit {
1347 self.should_quit = true;
1348 }
1349 }
1350 AppKeyAction::ActivateSlashPopup => {
1351 self.activate_slash_popup();
1352 }
1353 AppKeyAction::Custom(_) => {
1354 }
1358 }
1359 }
1360
1361 fn process_widget_action(&mut self, action: WidgetAction) {
1363 match action {
1364 WidgetAction::SubmitQuestion {
1365 tool_use_id,
1366 response,
1367 } => {
1368 self.submit_question_panel_response(tool_use_id, response);
1369 }
1370 WidgetAction::CancelQuestion { tool_use_id } => {
1371 self.cancel_question_panel_response(tool_use_id);
1372 }
1373 WidgetAction::SubmitPermission {
1374 tool_use_id,
1375 response,
1376 } => {
1377 self.submit_permission_panel_response(tool_use_id, response);
1378 }
1379 WidgetAction::CancelPermission { tool_use_id } => {
1380 self.cancel_permission_panel_response(tool_use_id);
1381 }
1382 WidgetAction::SubmitBatchPermission { batch_id, response } => {
1383 self.submit_batch_permission_response(batch_id, response);
1384 }
1385 WidgetAction::CancelBatchPermission { batch_id } => {
1386 self.cancel_batch_permission_response(batch_id);
1387 }
1388 WidgetAction::SwitchSession { session_id } => {
1389 self.switch_session(session_id);
1390 }
1391 WidgetAction::ExecuteCommand { command } => {
1392 if command.starts_with("__SLASH_INDEX_") {
1394 if let Ok(idx) = command
1395 .trim_start_matches("__SLASH_INDEX_")
1396 .parse::<usize>()
1397 {
1398 self.execute_slash_command_at_index(idx);
1399 }
1400 } else {
1401 self.execute_command(&command);
1402 }
1403 }
1404 WidgetAction::Close => {
1405 }
1407 }
1408 }
1409
1410 fn is_slash_popup_active(&self) -> bool {
1412 self.widgets
1413 .get(widget_ids::SLASH_POPUP)
1414 .map(|w| w.is_active())
1415 .unwrap_or(false)
1416 }
1417
1418 fn filter_command_indices(&self, prefix: &str) -> Vec<usize> {
1420 let search_term = prefix.trim_start_matches('/').to_lowercase();
1421 self.commands
1422 .iter()
1423 .enumerate()
1424 .filter(|(_, cmd)| cmd.name().to_lowercase().starts_with(&search_term))
1425 .map(|(i, _)| i)
1426 .collect()
1427 }
1428
1429 fn activate_slash_popup(&mut self) {
1431 let indices = self.filter_command_indices("/");
1433 let count = indices.len();
1434 self.filtered_command_indices = indices;
1435
1436 if let Some(widget) = self.widgets.get_mut(widget_ids::SLASH_POPUP)
1437 && let Some(popup) = widget.as_any_mut().downcast_mut::<SlashPopupState>()
1438 {
1439 popup.activate();
1440 popup.set_filtered_count(count);
1441 }
1442 }
1443
1444 fn handle_slash_popup_key(&mut self, key: KeyCode) {
1446 match key {
1447 KeyCode::Up => {
1448 if let Some(widget) = self.widgets.get_mut(widget_ids::SLASH_POPUP)
1449 && let Some(popup) = widget.as_any_mut().downcast_mut::<SlashPopupState>()
1450 {
1451 popup.select_previous();
1452 }
1453 }
1454 KeyCode::Down => {
1455 if let Some(widget) = self.widgets.get_mut(widget_ids::SLASH_POPUP)
1456 && let Some(popup) = widget.as_any_mut().downcast_mut::<SlashPopupState>()
1457 {
1458 popup.select_next();
1459 }
1460 }
1461 KeyCode::Enter => {
1462 let selected_idx = self
1463 .widgets
1464 .get(widget_ids::SLASH_POPUP)
1465 .and_then(|w| w.as_any().downcast_ref::<SlashPopupState>())
1466 .map(|p| p.selected_index)
1467 .unwrap_or(0);
1468 self.execute_slash_command_at_index(selected_idx);
1469 }
1470 KeyCode::Esc => {
1471 if let Some(input) = self.input_mut() {
1472 input.clear();
1473 }
1474 if let Some(widget) = self.widgets.get_mut(widget_ids::SLASH_POPUP)
1475 && let Some(popup) = widget.as_any_mut().downcast_mut::<SlashPopupState>()
1476 {
1477 popup.deactivate();
1478 }
1479 self.filtered_command_indices.clear();
1480 }
1481 KeyCode::Backspace => {
1482 let is_just_slash = self.input().map(|i| i.buffer() == "/").unwrap_or(false);
1483 if is_just_slash {
1484 if let Some(input) = self.input_mut() {
1485 input.clear();
1486 }
1487 if let Some(widget) = self.widgets.get_mut(widget_ids::SLASH_POPUP)
1488 && let Some(popup) = widget.as_any_mut().downcast_mut::<SlashPopupState>()
1489 {
1490 popup.deactivate();
1491 }
1492 self.filtered_command_indices.clear();
1493 } else {
1494 if let Some(input) = self.input_mut() {
1495 input.delete_char_before();
1496 }
1497 let buffer = self
1498 .input()
1499 .map(|i| i.buffer().to_string())
1500 .unwrap_or_default();
1501 self.filtered_command_indices = self.filter_command_indices(&buffer);
1502 if let Some(widget) = self.widgets.get_mut(widget_ids::SLASH_POPUP)
1503 && let Some(popup) = widget.as_any_mut().downcast_mut::<SlashPopupState>()
1504 {
1505 popup.set_filtered_count(self.filtered_command_indices.len());
1506 }
1507 }
1508 }
1509 KeyCode::Char(c) => {
1510 if let Some(input) = self.input_mut() {
1511 input.insert_char(c);
1512 }
1513 let buffer = self
1514 .input()
1515 .map(|i| i.buffer().to_string())
1516 .unwrap_or_default();
1517 self.filtered_command_indices = self.filter_command_indices(&buffer);
1518 if let Some(widget) = self.widgets.get_mut(widget_ids::SLASH_POPUP)
1519 && let Some(popup) = widget.as_any_mut().downcast_mut::<SlashPopupState>()
1520 {
1521 popup.set_filtered_count(self.filtered_command_indices.len());
1522 }
1523 }
1524 _ => {
1525 if let Some(widget) = self.widgets.get_mut(widget_ids::SLASH_POPUP)
1526 && let Some(popup) = widget.as_any_mut().downcast_mut::<SlashPopupState>()
1527 {
1528 popup.deactivate();
1529 }
1530 }
1531 }
1532 }
1533
1534 fn execute_slash_command_at_index(&mut self, idx: usize) {
1536 if let Some(&cmd_idx) = self.filtered_command_indices.get(idx)
1538 && let Some(cmd) = self.commands.get(cmd_idx)
1539 {
1540 let cmd_name = cmd.name().to_string();
1541 if let Some(input) = self.input_mut() {
1542 input.clear();
1543 for c in format!("/{}", cmd_name).chars() {
1544 input.insert_char(c);
1545 }
1546 }
1547 if let Some(widget) = self.widgets.get_mut(widget_ids::SLASH_POPUP)
1548 && let Some(popup) = widget.as_any_mut().downcast_mut::<SlashPopupState>()
1549 {
1550 popup.deactivate();
1551 }
1552 self.filtered_command_indices.clear();
1553 self.submit_message();
1554 }
1555 }
1556
1557 pub fn run(&mut self) -> io::Result<()> {
1558 enable_raw_mode()?;
1559 io::stdout().execute(EnterAlternateScreen)?;
1560 io::stdout().execute(EnableMouseCapture)?;
1561
1562 let mut terminal = Terminal::new(CrosstermBackend::new(io::stdout()))?;
1563
1564 while !self.should_quit {
1565 self.process_controller_messages();
1566
1567 let show_throbber = self.waiting_for_response
1568 || self.is_chat_streaming()
1569 || !self.executing_tools.is_empty();
1570
1571 if show_throbber {
1573 self.animation_frame_counter = self.animation_frame_counter.wrapping_add(1);
1574 if self.animation_frame_counter.is_multiple_of(6) {
1575 self.throbber_state.calc_next();
1576 self.conversation_view.step_spinner();
1577 }
1578 }
1579
1580 let prompt_len = PROMPT.chars().count();
1581 let indent_len = CONTINUATION_INDENT.len();
1582
1583 terminal.draw(|frame| {
1584 self.render_frame(frame, show_throbber, prompt_len, indent_len);
1585 })?;
1586
1587 let mut net_scroll: i32 = 0;
1589
1590 while event::poll(std::time::Duration::from_millis(0))? {
1591 match event::read()? {
1592 Event::Key(key) => {
1593 if key.kind == KeyEventKind::Press {
1594 self.handle_key(key.code, key.modifiers);
1595 }
1596 }
1597 Event::Mouse(mouse) => match mouse.kind {
1598 MouseEventKind::ScrollUp => net_scroll -= 1,
1599 MouseEventKind::ScrollDown => net_scroll += 1,
1600 _ => {}
1601 },
1602 _ => {}
1603 }
1604 }
1605
1606 if net_scroll < 0 {
1608 for _ in 0..(-net_scroll) {
1609 self.scroll_up();
1610 }
1611 } else if net_scroll > 0 {
1612 for _ in 0..net_scroll {
1613 self.scroll_down();
1614 }
1615 }
1616
1617 if net_scroll == 0 {
1618 std::thread::sleep(std::time::Duration::from_millis(16));
1619 }
1620 }
1621
1622 io::stdout().execute(DisableMouseCapture)?;
1623 disable_raw_mode()?;
1624 io::stdout().execute(LeaveAlternateScreen)?;
1625
1626 Ok(())
1627 }
1628
1629 fn render_frame(
1630 &mut self,
1631 frame: &mut ratatui::Frame,
1632 show_throbber: bool,
1633 prompt_len: usize,
1634 indent_len: usize,
1635 ) {
1636 let frame_area = frame.area();
1637 let frame_width = frame_area.width as usize;
1638 let frame_height = frame_area.height;
1639 let theme = app_theme();
1640
1641 let ctx =
1643 self.build_layout_context(frame_area, show_throbber, prompt_len, indent_len, &theme);
1644 let sizes = self.compute_widget_sizes(frame_height);
1645 let layout = self.layout_template.compute(&ctx, &sizes);
1646
1647 let theme_picker_active = sizes.is_active(widget_ids::THEME_PICKER);
1649 let session_picker_active = sizes.is_active(widget_ids::SESSION_PICKER);
1650 let question_panel_active = sizes.is_active(widget_ids::QUESTION_PANEL);
1651 let permission_panel_active = sizes.is_active(widget_ids::PERMISSION_PANEL);
1652 let batch_permission_panel_active = sizes.is_active(widget_ids::BATCH_PERMISSION_PANEL);
1653
1654 let status_bar_data = StatusBarData {
1656 cwd: self.get_cwd(),
1657 model_name: self.model_name.clone(),
1658 context_used: self.context_used,
1659 context_limit: self.context_limit,
1660 session_id: self.session_id,
1661 status_hint: self.key_handler.status_hint(),
1662 is_waiting: show_throbber,
1663 waiting_elapsed: self.waiting_started.map(|t| t.elapsed()),
1664 input_empty: self.input().map(|i| i.is_empty()).unwrap_or(true),
1665 panels_active: question_panel_active
1666 || permission_panel_active
1667 || batch_permission_panel_active,
1668 };
1669
1670 if let Some(widget) = self.widgets.get_mut(widget_ids::STATUS_BAR)
1672 && let Some(status_bar) = widget.as_any_mut().downcast_mut::<StatusBar>()
1673 {
1674 status_bar.update_data(status_bar_data);
1675 }
1676
1677 for widget_id in &layout.render_order {
1679 if *widget_id == widget_ids::THEME_PICKER || *widget_id == widget_ids::SESSION_PICKER {
1681 continue;
1682 }
1683
1684 let Some(area) = layout.widget_areas.get(widget_id) else {
1686 continue;
1687 };
1688
1689 match *widget_id {
1691 id if id == widget_ids::CHAT_VIEW => {
1692 let pending_status: Option<&str> = if !self.executing_tools.is_empty() {
1694 Some(PENDING_STATUS_TOOLS)
1695 } else if self.waiting_for_response && !self.is_chat_streaming() {
1696 Some(PENDING_STATUS_LLM)
1697 } else {
1698 None
1699 };
1700 self.conversation_view
1701 .render(frame, *area, &theme, pending_status);
1702 }
1703 id if id == widget_ids::TEXT_INPUT => {
1704 }
1706 id if id == widget_ids::SLASH_POPUP => {
1707 if let Some(widget) = self.widgets.get(widget_ids::SLASH_POPUP)
1708 && let Some(popup_state) = widget.as_any().downcast_ref::<SlashPopupState>()
1709 {
1710 let filtered: Vec<&dyn SlashCommand> = self
1712 .filtered_command_indices
1713 .iter()
1714 .filter_map(|&i| self.commands.get(i).map(|c| c.as_ref()))
1715 .collect();
1716 render_slash_popup(popup_state, &filtered, frame, *area, &theme);
1717 }
1718 }
1719 _ => {
1720 if let Some(widget) = self.widgets.get_mut(widget_id)
1722 && widget.is_active()
1723 {
1724 widget.render(frame, *area, &theme);
1725 }
1726 }
1727 }
1728 }
1729
1730 if let Some(input_area) = layout.input_area
1732 && !question_panel_active
1733 && !permission_panel_active
1734 && !batch_permission_panel_active
1735 {
1736 if show_throbber {
1737 let default_message;
1738 let message = if let Some(msg) = &self.custom_throbber_message {
1739 msg.as_str()
1740 } else if let Some(ref msg_fn) = self.processing_message_fn {
1741 default_message = msg_fn();
1742 &default_message
1743 } else {
1744 &self.processing_message
1745 };
1746 let throbber = Throbber::default()
1747 .label(message)
1748 .style(theme.throbber_label)
1749 .throbber_style(theme.throbber_spinner)
1750 .throbber_set(BRAILLE_EIGHT_DOUBLE);
1751
1752 let throbber_block = Block::default()
1753 .borders(Borders::TOP | Borders::BOTTOM)
1754 .border_style(theme.input_border);
1755 let inner = throbber_block.inner(input_area);
1756 let throbber_inner = Rect::new(
1757 inner.x + 1,
1758 inner.y,
1759 inner.width.saturating_sub(1),
1760 inner.height,
1761 );
1762 frame.render_widget(throbber_block, input_area);
1763 frame.render_stateful_widget(throbber, throbber_inner, &mut self.throbber_state);
1764 } else if let Some(input) = self.input() {
1765 let input_lines: Vec<String> = input
1766 .buffer()
1767 .split('\n')
1768 .enumerate()
1769 .map(|(i, line)| {
1770 if i == 0 {
1771 format!("{}{}", PROMPT, line)
1772 } else {
1773 format!("{}{}", CONTINUATION_INDENT, line)
1774 }
1775 })
1776 .collect();
1777 let input_text = if input_lines.is_empty() {
1778 PROMPT.to_string()
1779 } else {
1780 input_lines.join("\n")
1781 };
1782
1783 let input_box = Paragraph::new(input_text)
1784 .block(
1785 Block::default()
1786 .borders(Borders::TOP | Borders::BOTTOM)
1787 .border_style(theme.input_border),
1788 )
1789 .wrap(Wrap { trim: false });
1790 frame.render_widget(input_box, input_area);
1791
1792 if !theme_picker_active && !session_picker_active {
1794 let (cursor_rel_x, cursor_rel_y) =
1795 input.cursor_display_position_wrapped(frame_width, prompt_len, indent_len);
1796 let cursor_x = input_area.x + cursor_rel_x;
1797 let cursor_y = input_area.y + 1 + cursor_rel_y;
1798 frame.set_cursor_position((cursor_x, cursor_y));
1799 }
1800 }
1801 }
1802
1803 if theme_picker_active
1805 && let Some(widget) = self.widgets.get(widget_ids::THEME_PICKER)
1806 && let Some(picker) = widget.as_any().downcast_ref::<ThemePickerState>()
1807 {
1808 render_theme_picker(picker, frame, frame_area);
1809 }
1810
1811 if session_picker_active
1812 && let Some(widget) = self.widgets.get(widget_ids::SESSION_PICKER)
1813 && let Some(picker) = widget.as_any().downcast_ref::<SessionPickerState>()
1814 {
1815 render_session_picker(picker, frame, frame_area, &theme);
1816 }
1817 }
1818}
1819
1820impl Default for App {
1821 fn default() -> Self {
1822 Self::new()
1823 }
1824}