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 widgets::{Block, Borders, Paragraph, Wrap},
28 Terminal,
29};
30use throbber_widgets_tui::{Throbber, ThrobberState, BRAILLE_EIGHT_DOUBLE};
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::layout::{LayoutContext, LayoutTemplate, WidgetSizes};
41use super::themes::{render_theme_picker, ThemePickerState};
42use super::commands::{
43 is_slash_command, parse_command,
44 CommandContext, CommandResult, PendingAction, SlashCommand,
45};
46use super::keys::{AppKeyAction, AppKeyResult, DefaultKeyHandler, ExitHandler, KeyBindings, KeyContext, KeyHandler, NavigationHelper};
47use super::widgets::{
48 widget_ids, ChatView, TextInput, ToolStatus, SessionInfo, SessionPickerState,
49 SlashPopupState, Widget, WidgetAction, WidgetKeyContext, WidgetKeyResult, render_session_picker, render_slash_popup,
50 PermissionPanel, QuestionPanel, ConversationView, ConversationViewFactory,
51 StatusBar, StatusBarData, BatchPermissionPanel,
52};
53use super::{app_theme, current_theme_name, default_theme_name, get_theme, init_theme};
54
55const PROMPT: &str = " \u{203A} ";
56const CONTINUATION_INDENT: &str = " ";
57
58const PENDING_STATUS_TOOLS: &str = "running tools...";
60const PENDING_STATUS_LLM: &str = "Processing response from LLM...";
61
62pub type ProcessingMessageFn = Arc<dyn Fn() -> String + Send + Sync>;
65
66pub struct AppConfig {
68 pub agent_name: String,
70 pub version: String,
72 pub commands: Option<Vec<Box<dyn SlashCommand>>>,
74 pub command_extension: Option<Box<dyn std::any::Any + Send>>,
76 pub processing_message: String,
78 pub processing_message_fn: Option<ProcessingMessageFn>,
81 pub error_no_session: Option<String>,
83}
84
85impl std::fmt::Debug for AppConfig {
86 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87 f.debug_struct("AppConfig")
88 .field("agent_name", &self.agent_name)
89 .field("version", &self.version)
90 .field("commands", &self.commands.as_ref().map(|c| format!("<{} commands>", c.len())))
91 .field("command_extension", &self.command_extension.as_ref().map(|_| "<extension>"))
92 .field("processing_message", &self.processing_message)
93 .field("processing_message_fn", &self.processing_message_fn.as_ref().map(|_| "<fn>"))
94 .field("error_no_session", &self.error_no_session)
95 .finish()
96 }
97}
98
99impl Default for AppConfig {
100 fn default() -> Self {
101 Self {
102 agent_name: "Agent".to_string(),
103 version: "0.1.0".to_string(),
104 commands: None, command_extension: None,
106 processing_message: "Processing request...".to_string(),
107 processing_message_fn: None,
108 error_no_session: None,
109 }
110 }
111}
112
113pub struct App {
117 agent_name: String,
119
120 version: String,
122
123 commands: Vec<Box<dyn SlashCommand>>,
125
126 command_extension: Option<Box<dyn std::any::Any + Send>>,
128
129 processing_message: String,
131
132 processing_message_fn: Option<ProcessingMessageFn>,
134
135 error_no_session: Option<String>,
137
138 pub should_quit: bool,
140
141 to_controller: Option<ToControllerTx>,
143
144 from_controller: Option<FromControllerRx>,
146
147 controller: Option<Arc<LLMController>>,
149
150 llm_registry: Option<LLMRegistry>,
152
153 runtime_handle: Option<Handle>,
155
156 session_id: i64,
158
159 user_turn_counter: i64,
161
162 model_name: String,
164
165 context_used: i64,
167
168 context_limit: i32,
170
171 throbber_state: ThrobberState,
173
174 pub waiting_for_response: bool,
176
177 waiting_started: Option<Instant>,
179
180 animation_frame_counter: u8,
182
183 current_turn_id: Option<TurnId>,
185
186 executing_tools: HashSet<String>,
188
189 pub widgets: HashMap<&'static str, Box<dyn Widget>>,
191
192 pub widget_priority_order: Vec<&'static str>,
194
195 filtered_command_indices: Vec<usize>,
197
198 sessions: Vec<SessionInfo>,
200
201 session_states: HashMap<i64, Box<dyn std::any::Any + Send>>,
203
204 conversation_view: Box<dyn ConversationView>,
206
207 conversation_factory: ConversationViewFactory,
209
210 custom_throbber_message: Option<String>,
212
213 user_interaction_registry: Option<Arc<UserInteractionRegistry>>,
215
216 permission_registry: Option<Arc<PermissionRegistry>>,
218
219 layout_template: LayoutTemplate,
221
222 key_handler: Box<dyn KeyHandler>,
224
225 exit_handler: Option<Box<dyn ExitHandler>>,
227}
228
229impl App {
230 pub fn new() -> Self {
232 Self::with_config(AppConfig::default())
233 }
234
235 pub fn with_config(config: AppConfig) -> Self {
237 use super::commands::default_commands;
238
239 let theme_name = default_theme_name();
241 if let Some(theme) = get_theme(theme_name) {
242 init_theme(theme_name, theme);
243 }
244
245 let commands = config.commands.unwrap_or_else(default_commands);
247
248 let default_factory: ConversationViewFactory = Box::new(|| {
250 Box::new(ChatView::new())
251 });
252
253 let mut app = Self {
254 agent_name: config.agent_name,
255 version: config.version,
256 commands,
257 command_extension: config.command_extension,
258 processing_message: config.processing_message,
259 processing_message_fn: config.processing_message_fn,
260 error_no_session: config.error_no_session,
261 should_quit: false,
262 to_controller: None,
263 from_controller: None,
264 controller: None,
265 llm_registry: None,
266 runtime_handle: None,
267 session_id: 0,
268 user_turn_counter: 0,
269 model_name: "Not connected".to_string(),
270 context_used: 0,
271 context_limit: 0,
272 throbber_state: ThrobberState::default(),
273 waiting_for_response: false,
274 waiting_started: None,
275 animation_frame_counter: 0,
276 current_turn_id: None,
277 executing_tools: HashSet::new(),
278 widgets: HashMap::new(),
279 widget_priority_order: Vec::new(),
280 filtered_command_indices: Vec::new(),
281 sessions: Vec::new(),
282 session_states: HashMap::new(),
283 conversation_view: (default_factory)(),
284 conversation_factory: default_factory,
285 custom_throbber_message: None,
286 user_interaction_registry: None,
287 permission_registry: None,
288 layout_template: LayoutTemplate::default(),
289 key_handler: Box::new(DefaultKeyHandler::default()),
290 exit_handler: None,
291 };
292
293 app.register_widget(StatusBar::new());
295 app.register_widget(TextInput::new());
296
297 app
298 }
299
300 pub fn register_widget<W: Widget>(&mut self, widget: W) {
305 let id = widget.id();
306 self.widgets.insert(id, Box::new(widget));
307 self.rebuild_priority_order();
308 }
309
310 pub fn rebuild_priority_order(&mut self) {
312 let mut order: Vec<_> = self.widgets.keys().copied().collect();
313 order.sort_by(|a, b| {
314 let priority_a = self.widgets.get(a).map(|w| w.priority()).unwrap_or(0);
315 let priority_b = self.widgets.get(b).map(|w| w.priority()).unwrap_or(0);
316 priority_b.cmp(&priority_a) });
318 self.widget_priority_order = order;
319 }
320
321 pub fn widget<W: Widget + 'static>(&self, id: &str) -> Option<&W> {
323 self.widgets.get(id).and_then(|w| w.as_any().downcast_ref::<W>())
324 }
325
326 pub fn widget_mut<W: Widget + 'static>(&mut self, id: &str) -> Option<&mut W> {
328 self.widgets.get_mut(id).and_then(|w| w.as_any_mut().downcast_mut::<W>())
329 }
330
331 pub fn has_widget(&self, id: &str) -> bool {
333 self.widgets.contains_key(id)
334 }
335
336 fn any_widget_blocks_input(&self) -> bool {
338 self.widgets.values().any(|w| w.is_active() && w.blocks_input())
339 }
340
341 pub fn set_conversation_factory<F>(&mut self, factory: F)
357 where
358 F: Fn() -> Box<dyn ConversationView> + Send + Sync + 'static,
359 {
360 self.conversation_factory = Box::new(factory);
361 self.conversation_view = (self.conversation_factory)();
363 }
364
365 fn input(&self) -> Option<&TextInput> {
367 self.widget::<TextInput>(widget_ids::TEXT_INPUT)
368 }
369
370 fn input_mut(&mut self) -> Option<&mut TextInput> {
372 self.widget_mut::<TextInput>(widget_ids::TEXT_INPUT)
373 }
374
375 fn is_chat_streaming(&self) -> bool {
377 self.conversation_view.is_streaming()
378 }
379
380 pub fn agent_name(&self) -> &str {
382 &self.agent_name
383 }
384
385 pub fn version(&self) -> &str {
387 &self.version
388 }
389
390 pub fn set_to_controller(&mut self, tx: ToControllerTx) {
392 self.to_controller = Some(tx);
393 }
394
395 pub fn set_from_controller(&mut self, rx: FromControllerRx) {
397 self.from_controller = Some(rx);
398 }
399
400 pub fn set_controller(&mut self, controller: Arc<LLMController>) {
402 self.controller = Some(controller);
403 }
404
405 pub fn set_llm_registry(&mut self, registry: LLMRegistry) {
407 self.llm_registry = Some(registry);
408 }
409
410 pub fn set_runtime_handle(&mut self, handle: Handle) {
412 self.runtime_handle = Some(handle);
413 }
414
415 pub fn set_user_interaction_registry(&mut self, registry: Arc<UserInteractionRegistry>) {
417 self.user_interaction_registry = Some(registry);
418 }
419
420 pub fn set_permission_registry(&mut self, registry: Arc<PermissionRegistry>) {
422 self.permission_registry = Some(registry);
423 }
424
425 pub fn set_session_id(&mut self, id: i64) {
427 self.session_id = id;
428 }
429
430 pub fn set_model_name(&mut self, name: impl Into<String>) {
432 self.model_name = name.into();
433 }
434
435 pub fn set_context_limit(&mut self, limit: i32) {
437 self.context_limit = limit;
438 }
439
440 pub fn set_layout(&mut self, template: LayoutTemplate) {
442 self.layout_template = template;
443 }
444
445 pub fn set_key_handler<H: KeyHandler>(&mut self, handler: H) {
450 self.key_handler = Box::new(handler);
451 }
452
453 pub fn set_key_handler_boxed(&mut self, handler: Box<dyn KeyHandler>) {
457 self.key_handler = handler;
458 }
459
460 pub fn set_key_bindings(&mut self, bindings: KeyBindings) {
465 self.key_handler = Box::new(DefaultKeyHandler::new(bindings));
466 }
467
468 pub fn set_exit_handler<H: ExitHandler>(&mut self, handler: H) {
473 self.exit_handler = Some(Box::new(handler));
474 }
475
476 pub fn set_exit_handler_boxed(&mut self, handler: Box<dyn ExitHandler>) {
478 self.exit_handler = Some(handler);
479 }
480
481 fn compute_widget_sizes(&self, frame_height: u16) -> WidgetSizes {
483 let mut heights = HashMap::new();
484 let mut is_active = HashMap::new();
485
486 for (id, widget) in &self.widgets {
487 heights.insert(*id, widget.required_height(frame_height));
488 is_active.insert(*id, widget.is_active());
489 }
490
491 WidgetSizes { heights, is_active }
492 }
493
494 fn build_layout_context<'a>(
496 &self,
497 frame_area: Rect,
498 show_throbber: bool,
499 prompt_len: usize,
500 indent_len: usize,
501 theme: &'a super::themes::Theme,
502 ) -> LayoutContext<'a> {
503 let frame_width = frame_area.width as usize;
504 let input_visual_lines = self
505 .input()
506 .map(|i| i.visual_line_count(frame_width, prompt_len, indent_len))
507 .unwrap_or(1);
508
509 let mut active_widgets = HashSet::new();
510 for (id, widget) in &self.widgets {
511 if widget.is_active() {
512 active_widgets.insert(*id);
513 }
514 }
515
516 LayoutContext {
517 frame_area,
518 show_throbber,
519 input_visual_lines,
520 theme,
521 active_widgets,
522 }
523 }
524
525 pub fn submit_message(&mut self) {
526 let content = match self.input_mut() {
528 Some(input) => input.take(),
529 None => return, };
531 if content.trim().is_empty() {
532 return;
533 }
534
535 if is_slash_command(&content) {
537 self.execute_command(&content);
538 return;
539 }
540
541 self.conversation_view.enable_auto_scroll();
543 self.conversation_view.add_user_message(content.clone());
544
545 if self.session_id == 0 {
547 let msg = self.error_no_session.clone()
548 .unwrap_or_else(|| "No active session. Use /new-session to create one.".to_string());
549 self.conversation_view.add_system_message(msg);
550 return;
551 }
552
553 if let Some(ref tx) = self.to_controller {
555 self.user_turn_counter += 1;
556 let turn_id = TurnId::new_user_turn(self.user_turn_counter);
557 let payload = ControllerInputPayload::data(self.session_id, content, turn_id);
558
559 if tx.try_send(payload).is_err() {
561 self.conversation_view.add_system_message("Failed to send message to controller".to_string());
562 } else {
563 self.waiting_for_response = true;
565 self.waiting_started = Some(Instant::now());
566 self.current_turn_id = Some(TurnId::new_user_turn(self.user_turn_counter));
568 }
569 }
570 }
571
572 pub fn interrupt_request(&mut self) {
574 if !self.waiting_for_response
576 && !self.is_chat_streaming()
577 && self.executing_tools.is_empty()
578 {
579 return;
580 }
581
582 if let Some(ref tx) = self.to_controller {
584 let payload = ControllerInputPayload::control(self.session_id, ControlCmd::Interrupt);
585 if tx.try_send(payload).is_ok() {
586 self.waiting_for_response = false;
588 self.waiting_started = None;
589 self.executing_tools.clear();
590 self.conversation_view.complete_streaming();
592 self.current_turn_id = None;
594 self.conversation_view.add_system_message("Request cancelled".to_string());
595 }
596 }
597 }
598
599 fn execute_command(&mut self, input: &str) {
601 let Some((cmd_name, args)) = parse_command(input) else {
602 self.conversation_view.add_system_message("Invalid command format".to_string());
603 return;
604 };
605
606 let cmd_idx = self.commands.iter().position(|c| c.name() == cmd_name);
608 let Some(cmd_idx) = cmd_idx else {
609 self.conversation_view.add_system_message(format!("Unknown command: /{}", cmd_name));
610 return;
611 };
612
613 let (result, pending_actions) = {
615 let extension = self.command_extension.take();
617 let extension_ref = extension.as_ref().map(|e| e.as_ref() as &dyn std::any::Any);
618
619 let mut ctx = CommandContext::new(
620 self.session_id,
621 &self.agent_name,
622 &self.version,
623 &self.commands,
624 &mut *self.conversation_view,
625 self.to_controller.as_ref(),
626 extension_ref,
627 );
628
629 let result = self.commands[cmd_idx].execute(args, &mut ctx);
631 let pending_actions = ctx.take_pending_actions();
632
633 self.command_extension = extension;
635
636 (result, pending_actions)
637 };
638
639 for action in pending_actions {
641 match action {
642 PendingAction::OpenThemePicker => self.cmd_themes(),
643 PendingAction::OpenSessionPicker => self.cmd_sessions(),
644 PendingAction::ClearConversation => self.cmd_clear(),
645 PendingAction::CompactConversation => self.cmd_compact(),
646 PendingAction::CreateNewSession => { self.cmd_new_session(); }
647 PendingAction::Quit => { self.should_quit = true; }
648 }
649 }
650
651 match result {
653 CommandResult::Ok | CommandResult::Handled => {}
654 CommandResult::Message(msg) => {
655 self.conversation_view.add_system_message(msg);
656 }
657 CommandResult::Error(err) => {
658 self.conversation_view.add_system_message(format!("Error: {}", err));
659 }
660 CommandResult::Quit => {
661 self.should_quit = true;
662 }
663 }
664 }
665
666 fn cmd_clear(&mut self) {
667 self.conversation_view = (self.conversation_factory)();
669 self.user_turn_counter = 0;
670
671 if self.session_id != 0 {
673 if let Some(ref tx) = self.to_controller {
674 let payload =
675 ControllerInputPayload::control(self.session_id, ControlCmd::Clear);
676 if let Err(e) = tx.try_send(payload) {
677 tracing::warn!("Failed to send clear command to controller: {}", e);
678 }
679 }
680 }
681 }
682
683 fn cmd_compact(&mut self) {
684 if self.session_id == 0 {
686 self.conversation_view.add_system_message("No active session to compact".to_string());
687 return;
688 }
689
690 if let Some(ref tx) = self.to_controller {
692 let payload = ControllerInputPayload::control(self.session_id, ControlCmd::Compact);
693 if tx.try_send(payload).is_ok() {
694 self.waiting_for_response = true;
696 self.waiting_started = Some(Instant::now());
697 self.custom_throbber_message = Some("compacting...".to_string());
698 } else {
699 self.conversation_view.add_system_message("Failed to send compact command".to_string());
700 }
701 }
702 }
703
704 fn cmd_new_session(&mut self) -> String {
705 let Some(ref controller) = self.controller else {
706 return "Error: Controller not available".to_string();
707 };
708
709 let Some(ref handle) = self.runtime_handle else {
710 return "Error: Runtime not available".to_string();
711 };
712
713 let Some(ref registry) = self.llm_registry else {
714 return "Error: No LLM providers configured.\nSet ANTHROPIC_API_KEY or create config file".to_string();
715 };
716
717 let Some(config) = registry.get_default() else {
719 return "Error: No LLM providers configured.\nSet ANTHROPIC_API_KEY or create config file".to_string();
720 };
721
722 let model = config.model.clone();
723 let context_limit = config.context_limit;
724 let config = config.clone();
725
726 let controller = controller.clone();
728 let session_id = match handle.block_on(async { controller.create_session(config).await }) {
729 Ok(id) => id,
730 Err(e) => {
731 return format!("Error: Failed to create session: {}", e);
732 }
733 };
734
735 let session_info = SessionInfo::new(session_id, model.clone(), context_limit);
737 self.sessions.push(session_info);
738
739 if self.session_id != 0 {
741 let state = self.conversation_view.save_state();
742 self.session_states.insert(self.session_id, state);
743 }
744
745 self.conversation_view = (self.conversation_factory)();
747
748 self.session_id = session_id;
749 self.model_name = model.clone();
750 self.context_limit = context_limit;
751 self.context_used = 0;
752 self.user_turn_counter = 0;
753
754 String::new()
756 }
757
758 fn cmd_themes(&mut self) {
759 if let Some(widget) = self.widgets.get_mut(widget_ids::THEME_PICKER) {
760 if let Some(picker) = widget.as_any_mut().downcast_mut::<ThemePickerState>() {
761 let current_name = current_theme_name();
762 let current_theme = app_theme();
763 picker.activate(¤t_name, current_theme);
764 }
765 }
766 }
767
768 fn cmd_sessions(&mut self) {
769 if let Some(session) = self.sessions.iter_mut().find(|s| s.id == self.session_id) {
771 session.context_used = self.context_used;
772 }
773
774 if let Some(widget) = self.widgets.get_mut(widget_ids::SESSION_PICKER) {
775 if let Some(picker) = widget.as_any_mut().downcast_mut::<SessionPickerState>() {
776 picker.activate(self.sessions.clone(), self.session_id);
777 }
778 }
779 }
780
781 pub fn switch_session(&mut self, session_id: i64) {
783 if session_id == self.session_id {
785 return;
786 }
787
788 if let Some(session) = self.sessions.iter_mut().find(|s| s.id == self.session_id) {
790 session.context_used = self.context_used;
791 }
792
793 let state = self.conversation_view.save_state();
795 self.session_states.insert(self.session_id, state);
796
797 if let Some(session) = self.sessions.iter().find(|s| s.id == session_id) {
799 self.session_id = session_id;
800 self.model_name = session.model.clone();
801 self.context_used = session.context_used;
802 self.context_limit = session.context_limit;
803 self.user_turn_counter = 0;
804
805 if let Some(stored_state) = self.session_states.remove(&session_id) {
807 self.conversation_view.restore_state(stored_state);
808 } else {
809 self.conversation_view = (self.conversation_factory)();
810 }
811 }
812 }
813
814 pub fn add_session(&mut self, info: SessionInfo) {
816 self.sessions.push(info);
817 }
818
819 fn submit_question_panel_response(&mut self, tool_use_id: String, response: crate::controller::AskUserQuestionsResponse) {
821 if let (Some(registry), Some(handle)) =
823 (&self.user_interaction_registry, &self.runtime_handle)
824 {
825 let registry = registry.clone();
826 handle.spawn(async move {
827 if let Err(e) = registry.respond(&tool_use_id, response).await {
828 tracing::error!(%tool_use_id, ?e, "Failed to respond to interaction");
829 }
830 });
831 }
832
833 if let Some(widget) = self.widgets.get_mut(widget_ids::QUESTION_PANEL) {
835 if let Some(panel) = widget.as_any_mut().downcast_mut::<QuestionPanel>() {
836 panel.deactivate();
837 }
838 }
839 }
840
841 fn cancel_question_panel_response(&mut self, tool_use_id: String) {
843 if let (Some(registry), Some(handle)) =
845 (&self.user_interaction_registry, &self.runtime_handle)
846 {
847 let registry = registry.clone();
848 handle.spawn(async move {
849 if let Err(e) = registry.cancel(&tool_use_id).await {
850 tracing::warn!(%tool_use_id, ?e, "Failed to cancel interaction");
851 }
852 });
853 }
854
855 if let Some(widget) = self.widgets.get_mut(widget_ids::QUESTION_PANEL) {
857 if let Some(panel) = widget.as_any_mut().downcast_mut::<QuestionPanel>() {
858 panel.deactivate();
859 }
860 }
861 }
862
863 fn submit_permission_panel_response(&mut self, tool_use_id: String, response: PermissionPanelResponse) {
865 if let (Some(registry), Some(handle)) = (&self.permission_registry, &self.runtime_handle) {
867 let registry = registry.clone();
868 handle.spawn(async move {
869 if let Err(e) = registry.respond_to_request(&tool_use_id, response).await {
870 tracing::error!(%tool_use_id, ?e, "Failed to respond to permission request");
871 }
872 });
873 }
874
875 if let Some(widget) = self.widgets.get_mut(widget_ids::PERMISSION_PANEL) {
877 if let Some(panel) = widget.as_any_mut().downcast_mut::<PermissionPanel>() {
878 panel.deactivate();
879 }
880 }
881 }
882
883 fn cancel_permission_panel_response(&mut self, tool_use_id: String) {
885 if let (Some(registry), Some(handle)) = (&self.permission_registry, &self.runtime_handle) {
887 let registry = registry.clone();
888 handle.spawn(async move {
889 if let Err(e) = registry.cancel(&tool_use_id).await {
890 tracing::warn!(%tool_use_id, ?e, "Failed to cancel permission request");
891 }
892 });
893 }
894
895 if let Some(widget) = self.widgets.get_mut(widget_ids::PERMISSION_PANEL) {
897 if let Some(panel) = widget.as_any_mut().downcast_mut::<PermissionPanel>() {
898 panel.deactivate();
899 }
900 }
901 }
902
903 fn submit_batch_permission_response(
905 &mut self,
906 batch_id: String,
907 response: crate::permissions::BatchPermissionResponse,
908 ) {
909 if let (Some(registry), Some(handle)) = (&self.permission_registry, &self.runtime_handle) {
911 let registry = registry.clone();
912 handle.spawn(async move {
913 if let Err(e) = registry.respond_to_batch(&batch_id, response).await {
914 tracing::error!(%batch_id, ?e, "Failed to respond to batch permission request");
915 }
916 });
917 }
918
919 if let Some(widget) = self.widgets.get_mut(widget_ids::BATCH_PERMISSION_PANEL) {
921 if let Some(panel) = widget
922 .as_any_mut()
923 .downcast_mut::<crate::widgets::BatchPermissionPanel>()
924 {
925 panel.deactivate();
926 }
927 }
928 }
929
930 fn cancel_batch_permission_response(&mut self, batch_id: String) {
932 if let (Some(registry), Some(handle)) = (&self.permission_registry, &self.runtime_handle) {
934 let registry = registry.clone();
935 handle.spawn(async move {
936 if let Err(e) = registry.cancel_batch(&batch_id).await {
937 tracing::warn!(%batch_id, ?e, "Failed to cancel batch permission request");
938 }
939 });
940 }
941
942 if let Some(widget) = self.widgets.get_mut(widget_ids::BATCH_PERMISSION_PANEL) {
944 if let Some(panel) = widget
945 .as_any_mut()
946 .downcast_mut::<crate::widgets::BatchPermissionPanel>()
947 {
948 panel.deactivate();
949 }
950 }
951 }
952
953 fn process_controller_messages(&mut self) {
955 let mut messages = Vec::new();
957
958 if let Some(ref mut rx) = self.from_controller {
959 loop {
960 match rx.try_recv() {
961 Ok(msg) => messages.push(msg),
962 Err(mpsc::error::TryRecvError::Empty) => break,
963 Err(mpsc::error::TryRecvError::Disconnected) => {
964 tracing::warn!("Controller channel disconnected");
965 break;
966 }
967 }
968 }
969 }
970
971 for msg in messages {
973 self.handle_ui_message(msg);
974 }
975 }
976
977 fn handle_ui_message(&mut self, msg: UiMessage) {
979 match msg {
980 UiMessage::TextChunk { text, turn_id, .. } => {
981 if !self.is_current_turn(&turn_id) {
983 return;
984 }
985 self.conversation_view.append_streaming(&text);
986 }
987 UiMessage::Display { message, .. } => {
988 self.conversation_view.add_system_message(message);
989 }
990 UiMessage::Complete {
991 turn_id,
992 stop_reason,
993 ..
994 } => {
995 if !self.is_current_turn(&turn_id) {
997 return;
998 }
999
1000 let is_tool_use = stop_reason.as_deref() == Some("tool_use");
1002
1003 self.conversation_view.complete_streaming();
1004
1005 if !is_tool_use {
1007 self.waiting_for_response = false;
1008 self.waiting_started = None;
1009 }
1010 }
1011 UiMessage::TokenUpdate {
1012 input_tokens,
1013 context_limit,
1014 ..
1015 } => {
1016 self.context_used = input_tokens;
1017 self.context_limit = context_limit;
1018 }
1019 UiMessage::Error { error, turn_id, .. } => {
1020 let should_process = self.is_current_turn(&turn_id)
1023 || (self.current_turn_id.is_some() && turn_id.is_none());
1024 if !should_process {
1025 return;
1026 }
1027 self.conversation_view.complete_streaming();
1028 self.waiting_for_response = false;
1029 self.waiting_started = None;
1030 self.current_turn_id = None;
1031 self.conversation_view.add_system_message(format!("Error: {}", error));
1032 }
1033 UiMessage::System { message, .. } => {
1034 self.conversation_view.add_system_message(message);
1035 }
1036 UiMessage::ToolExecuting {
1037 tool_use_id,
1038 display_name,
1039 display_title,
1040 ..
1041 } => {
1042 self.executing_tools.insert(tool_use_id.clone());
1043 self.conversation_view.add_tool_message(&tool_use_id, &display_name, &display_title);
1044 }
1045 UiMessage::ToolCompleted {
1046 tool_use_id,
1047 status,
1048 error,
1049 ..
1050 } => {
1051 self.executing_tools.remove(&tool_use_id);
1052 let tool_status = if status == ToolResultStatus::Success {
1053 ToolStatus::Completed
1054 } else {
1055 ToolStatus::Failed(error.unwrap_or_default())
1056 };
1057 self.conversation_view.update_tool_status(&tool_use_id, tool_status);
1058 }
1059 UiMessage::CommandComplete {
1060 command,
1061 success,
1062 message,
1063 ..
1064 } => {
1065 self.waiting_for_response = false;
1066 self.waiting_started = None;
1067 self.custom_throbber_message = None;
1068
1069 match command {
1070 ControlCmd::Compact => {
1071 if let Some(msg) = message {
1072 self.conversation_view.add_system_message(msg);
1073 }
1074 }
1075 ControlCmd::Clear => {}
1076 _ => {
1077 tracing::debug!(?command, ?success, "Command completed");
1078 }
1079 }
1080 }
1081 UiMessage::UserInteractionRequired {
1082 session_id,
1083 tool_use_id,
1084 request,
1085 turn_id,
1086 } => {
1087 if session_id == self.session_id {
1088 self.conversation_view.update_tool_status(&tool_use_id, ToolStatus::WaitingForUser);
1089 if let Some(widget) = self.widgets.get_mut(widget_ids::QUESTION_PANEL) {
1091 if let Some(panel) = widget.as_any_mut().downcast_mut::<QuestionPanel>() {
1092 panel.activate(tool_use_id, session_id, request, turn_id);
1093 }
1094 }
1095 }
1096 }
1097 UiMessage::PermissionRequired {
1098 session_id,
1099 tool_use_id,
1100 request,
1101 turn_id,
1102 } => {
1103 if session_id == self.session_id {
1104 self.conversation_view.update_tool_status(&tool_use_id, ToolStatus::WaitingForUser);
1105 if let Some(widget) = self.widgets.get_mut(widget_ids::PERMISSION_PANEL) {
1107 if let Some(panel) = widget.as_any_mut().downcast_mut::<PermissionPanel>() {
1108 panel.activate(tool_use_id, session_id, request, turn_id);
1109 }
1110 }
1111 }
1112 }
1113 UiMessage::BatchPermissionRequired {
1114 session_id,
1115 batch,
1116 turn_id,
1117 } => {
1118 if session_id == self.session_id {
1119 for request in &batch.requests {
1121 self.conversation_view.update_tool_status(&request.id, ToolStatus::WaitingForUser);
1122 }
1123 if let Some(widget) = self.widgets.get_mut(widget_ids::BATCH_PERMISSION_PANEL) {
1125 if let Some(panel) = widget.as_any_mut().downcast_mut::<BatchPermissionPanel>() {
1126 panel.activate(session_id, batch, turn_id);
1127 }
1128 }
1129 }
1130 }
1131 }
1132 }
1133
1134 fn is_current_turn(&self, turn_id: &Option<TurnId>) -> bool {
1136 match (&self.current_turn_id, turn_id) {
1137 (Some(current), Some(incoming)) => current == incoming,
1138 (None, _) => false,
1139 (Some(_), None) => false,
1140 }
1141 }
1142
1143 pub fn scroll_up(&mut self) {
1144 self.conversation_view.scroll_up();
1145 }
1146
1147 pub fn scroll_down(&mut self) {
1148 self.conversation_view.scroll_down();
1149 }
1150
1151 fn get_cwd(&self) -> String {
1153 std::env::current_dir()
1154 .map(|p| {
1155 let path_str = p.display().to_string();
1156 if let Some(home) = std::env::var_os("HOME") {
1157 let home_str = home.to_string_lossy();
1158 if path_str.starts_with(home_str.as_ref()) {
1159 return format!("~{}", &path_str[home_str.len()..]);
1160 }
1161 }
1162 path_str
1163 })
1164 .unwrap_or_else(|_| "unknown".to_string())
1165 }
1166
1167 fn handle_key(&mut self, key: KeyCode, modifiers: KeyModifiers) {
1169 let key_event = KeyEvent::new(key, modifiers);
1171 let context = KeyContext {
1172 input_empty: self.input().map(|i| i.is_empty()).unwrap_or(true),
1173 is_processing: self.waiting_for_response || self.is_chat_streaming(),
1174 widget_blocking: self.any_widget_blocks_input(),
1175 };
1176
1177 let result = self.key_handler.handle_key(key_event, &context);
1180
1181 match result {
1182 AppKeyResult::Handled => return,
1183 AppKeyResult::Action(action) => {
1184 self.execute_key_action(action);
1185 return;
1186 }
1187 AppKeyResult::NotHandled => {
1188 }
1190 }
1191
1192 let theme = app_theme();
1194 let nav = NavigationHelper::new(self.key_handler.bindings());
1195 let widget_ctx = WidgetKeyContext { theme: &theme, nav };
1196
1197 let widget_ids_to_check: Vec<&'static str> = self.widget_priority_order.clone();
1199
1200 for widget_id in widget_ids_to_check {
1201 if let Some(widget) = self.widgets.get_mut(widget_id) {
1202 if widget.is_active() {
1203 match widget.handle_key(key_event, &widget_ctx) {
1204 WidgetKeyResult::Handled => return,
1205 WidgetKeyResult::Action(action) => {
1206 self.process_widget_action(action);
1207 return;
1208 }
1209 WidgetKeyResult::NotHandled => {
1210 }
1212 }
1213 }
1214 }
1215 }
1216
1217 if self.is_slash_popup_active() {
1219 self.handle_slash_popup_key(key);
1220 return;
1221 }
1222 }
1223
1224 fn execute_key_action(&mut self, action: AppKeyAction) {
1226 match action {
1227 AppKeyAction::MoveUp => {
1228 if let Some(input) = self.input_mut() {
1229 input.move_up();
1230 }
1231 }
1232 AppKeyAction::MoveDown => {
1233 if let Some(input) = self.input_mut() {
1234 input.move_down();
1235 }
1236 }
1237 AppKeyAction::MoveLeft => {
1238 if let Some(input) = self.input_mut() {
1239 input.move_left();
1240 }
1241 }
1242 AppKeyAction::MoveRight => {
1243 if let Some(input) = self.input_mut() {
1244 input.move_right();
1245 }
1246 }
1247 AppKeyAction::MoveLineStart => {
1248 if let Some(input) = self.input_mut() {
1249 input.move_to_line_start();
1250 }
1251 }
1252 AppKeyAction::MoveLineEnd => {
1253 if let Some(input) = self.input_mut() {
1254 input.move_to_line_end();
1255 }
1256 }
1257 AppKeyAction::DeleteCharBefore => {
1258 if let Some(input) = self.input_mut() {
1259 input.delete_char_before();
1260 }
1261 }
1262 AppKeyAction::DeleteCharAt => {
1263 if let Some(input) = self.input_mut() {
1264 input.delete_char_at();
1265 }
1266 }
1267 AppKeyAction::KillLine => {
1268 if let Some(input) = self.input_mut() {
1269 input.kill_line();
1270 }
1271 }
1272 AppKeyAction::InsertNewline => {
1273 if let Some(input) = self.input_mut() {
1274 input.insert_char('\n');
1275 }
1276 }
1277 AppKeyAction::InsertChar(c) => {
1278 if let Some(input) = self.input_mut() {
1279 input.insert_char(c);
1280 }
1281 if c == '/' && self.input().map(|i| i.buffer() == "/").unwrap_or(false) {
1283 self.activate_slash_popup();
1284 }
1285 }
1286 AppKeyAction::Submit => {
1287 self.submit_message();
1288 }
1289 AppKeyAction::Interrupt => {
1290 self.interrupt_request();
1291 }
1292 AppKeyAction::Quit => {
1293 self.should_quit = true;
1294 }
1295 AppKeyAction::RequestExit => {
1296 let should_quit = self.exit_handler
1298 .as_mut()
1299 .map(|h| h.on_exit())
1300 .unwrap_or(true);
1301 if should_quit {
1302 self.should_quit = true;
1303 }
1304 }
1305 AppKeyAction::ActivateSlashPopup => {
1306 self.activate_slash_popup();
1307 }
1308 AppKeyAction::Custom(_) => {
1309 }
1313 }
1314 }
1315
1316 fn process_widget_action(&mut self, action: WidgetAction) {
1318 match action {
1319 WidgetAction::SubmitQuestion { tool_use_id, response } => {
1320 self.submit_question_panel_response(tool_use_id, response);
1321 }
1322 WidgetAction::CancelQuestion { tool_use_id } => {
1323 self.cancel_question_panel_response(tool_use_id);
1324 }
1325 WidgetAction::SubmitPermission { tool_use_id, response } => {
1326 self.submit_permission_panel_response(tool_use_id, response);
1327 }
1328 WidgetAction::CancelPermission { tool_use_id } => {
1329 self.cancel_permission_panel_response(tool_use_id);
1330 }
1331 WidgetAction::SubmitBatchPermission { batch_id, response } => {
1332 self.submit_batch_permission_response(batch_id, response);
1333 }
1334 WidgetAction::CancelBatchPermission { batch_id } => {
1335 self.cancel_batch_permission_response(batch_id);
1336 }
1337 WidgetAction::SwitchSession { session_id } => {
1338 self.switch_session(session_id);
1339 }
1340 WidgetAction::ExecuteCommand { command } => {
1341 if command.starts_with("__SLASH_INDEX_") {
1343 if let Ok(idx) = command.trim_start_matches("__SLASH_INDEX_").parse::<usize>() {
1344 self.execute_slash_command_at_index(idx);
1345 }
1346 } else {
1347 self.execute_command(&command);
1348 }
1349 }
1350 WidgetAction::Close => {
1351 }
1353 }
1354 }
1355
1356 fn is_slash_popup_active(&self) -> bool {
1358 self.widgets
1359 .get(widget_ids::SLASH_POPUP)
1360 .map(|w| w.is_active())
1361 .unwrap_or(false)
1362 }
1363
1364 fn filter_command_indices(&self, prefix: &str) -> Vec<usize> {
1366 let search_term = prefix.trim_start_matches('/').to_lowercase();
1367 self.commands
1368 .iter()
1369 .enumerate()
1370 .filter(|(_, cmd)| cmd.name().to_lowercase().starts_with(&search_term))
1371 .map(|(i, _)| i)
1372 .collect()
1373 }
1374
1375 fn activate_slash_popup(&mut self) {
1377 let indices = self.filter_command_indices("/");
1379 let count = indices.len();
1380 self.filtered_command_indices = indices;
1381
1382 if let Some(widget) = self.widgets.get_mut(widget_ids::SLASH_POPUP) {
1383 if let Some(popup) = widget.as_any_mut().downcast_mut::<SlashPopupState>() {
1384 popup.activate();
1385 popup.set_filtered_count(count);
1386 }
1387 }
1388 }
1389
1390 fn handle_slash_popup_key(&mut self, key: KeyCode) {
1392 match key {
1393 KeyCode::Up => {
1394 if let Some(widget) = self.widgets.get_mut(widget_ids::SLASH_POPUP) {
1395 if let Some(popup) = widget.as_any_mut().downcast_mut::<SlashPopupState>() {
1396 popup.select_previous();
1397 }
1398 }
1399 }
1400 KeyCode::Down => {
1401 if let Some(widget) = self.widgets.get_mut(widget_ids::SLASH_POPUP) {
1402 if let Some(popup) = widget.as_any_mut().downcast_mut::<SlashPopupState>() {
1403 popup.select_next();
1404 }
1405 }
1406 }
1407 KeyCode::Enter => {
1408 let selected_idx = self.widgets
1409 .get(widget_ids::SLASH_POPUP)
1410 .and_then(|w| w.as_any().downcast_ref::<SlashPopupState>())
1411 .map(|p| p.selected_index)
1412 .unwrap_or(0);
1413 self.execute_slash_command_at_index(selected_idx);
1414 }
1415 KeyCode::Esc => {
1416 if let Some(input) = self.input_mut() {
1417 input.clear();
1418 }
1419 if let Some(widget) = self.widgets.get_mut(widget_ids::SLASH_POPUP) {
1420 if let Some(popup) = widget.as_any_mut().downcast_mut::<SlashPopupState>() {
1421 popup.deactivate();
1422 }
1423 }
1424 self.filtered_command_indices.clear();
1425 }
1426 KeyCode::Backspace => {
1427 let is_just_slash = self.input().map(|i| i.buffer() == "/").unwrap_or(false);
1428 if is_just_slash {
1429 if let Some(input) = self.input_mut() {
1430 input.clear();
1431 }
1432 if let Some(widget) = self.widgets.get_mut(widget_ids::SLASH_POPUP) {
1433 if let Some(popup) = widget.as_any_mut().downcast_mut::<SlashPopupState>() {
1434 popup.deactivate();
1435 }
1436 }
1437 self.filtered_command_indices.clear();
1438 } else {
1439 if let Some(input) = self.input_mut() {
1440 input.delete_char_before();
1441 }
1442 let buffer = self.input().map(|i| i.buffer().to_string()).unwrap_or_default();
1443 self.filtered_command_indices = self.filter_command_indices(&buffer);
1444 if let Some(widget) = self.widgets.get_mut(widget_ids::SLASH_POPUP) {
1445 if let Some(popup) = widget.as_any_mut().downcast_mut::<SlashPopupState>() {
1446 popup.set_filtered_count(self.filtered_command_indices.len());
1447 }
1448 }
1449 }
1450 }
1451 KeyCode::Char(c) => {
1452 if let Some(input) = self.input_mut() {
1453 input.insert_char(c);
1454 }
1455 let buffer = self.input().map(|i| i.buffer().to_string()).unwrap_or_default();
1456 self.filtered_command_indices = self.filter_command_indices(&buffer);
1457 if let Some(widget) = self.widgets.get_mut(widget_ids::SLASH_POPUP) {
1458 if let Some(popup) = widget.as_any_mut().downcast_mut::<SlashPopupState>() {
1459 popup.set_filtered_count(self.filtered_command_indices.len());
1460 }
1461 }
1462 }
1463 _ => {
1464 if let Some(widget) = self.widgets.get_mut(widget_ids::SLASH_POPUP) {
1465 if let Some(popup) = widget.as_any_mut().downcast_mut::<SlashPopupState>() {
1466 popup.deactivate();
1467 }
1468 }
1469 }
1470 }
1471 }
1472
1473 fn execute_slash_command_at_index(&mut self, idx: usize) {
1475 if let Some(&cmd_idx) = self.filtered_command_indices.get(idx) {
1477 if let Some(cmd) = self.commands.get(cmd_idx) {
1478 let cmd_name = cmd.name().to_string();
1479 if let Some(input) = self.input_mut() {
1480 input.clear();
1481 for c in format!("/{}", cmd_name).chars() {
1482 input.insert_char(c);
1483 }
1484 }
1485 if let Some(widget) = self.widgets.get_mut(widget_ids::SLASH_POPUP) {
1486 if let Some(popup) = widget.as_any_mut().downcast_mut::<SlashPopupState>() {
1487 popup.deactivate();
1488 }
1489 }
1490 self.filtered_command_indices.clear();
1491 self.submit_message();
1492 }
1493 }
1494 }
1495
1496 pub fn run(&mut self) -> io::Result<()> {
1497 enable_raw_mode()?;
1498 io::stdout().execute(EnterAlternateScreen)?;
1499 io::stdout().execute(EnableMouseCapture)?;
1500
1501 let mut terminal = Terminal::new(CrosstermBackend::new(io::stdout()))?;
1502
1503 while !self.should_quit {
1504 self.process_controller_messages();
1505
1506 let show_throbber = self.waiting_for_response
1507 || self.is_chat_streaming()
1508 || !self.executing_tools.is_empty();
1509
1510 if show_throbber {
1512 self.animation_frame_counter = self.animation_frame_counter.wrapping_add(1);
1513 if self.animation_frame_counter % 6 == 0 {
1514 self.throbber_state.calc_next();
1515 self.conversation_view.step_spinner();
1516 }
1517 }
1518
1519 let prompt_len = PROMPT.chars().count();
1520 let indent_len = CONTINUATION_INDENT.len();
1521
1522 terminal.draw(|frame| {
1523 self.render_frame(frame, show_throbber, prompt_len, indent_len);
1524 })?;
1525
1526 let mut net_scroll: i32 = 0;
1528
1529 while event::poll(std::time::Duration::from_millis(0))? {
1530 match event::read()? {
1531 Event::Key(key) => {
1532 if key.kind == KeyEventKind::Press {
1533 self.handle_key(key.code, key.modifiers);
1534 }
1535 }
1536 Event::Mouse(mouse) => match mouse.kind {
1537 MouseEventKind::ScrollUp => net_scroll -= 1,
1538 MouseEventKind::ScrollDown => net_scroll += 1,
1539 _ => {}
1540 },
1541 _ => {}
1542 }
1543 }
1544
1545 if net_scroll < 0 {
1547 for _ in 0..(-net_scroll) {
1548 self.scroll_up();
1549 }
1550 } else if net_scroll > 0 {
1551 for _ in 0..net_scroll {
1552 self.scroll_down();
1553 }
1554 }
1555
1556 if net_scroll == 0 {
1557 std::thread::sleep(std::time::Duration::from_millis(16));
1558 }
1559 }
1560
1561 io::stdout().execute(DisableMouseCapture)?;
1562 disable_raw_mode()?;
1563 io::stdout().execute(LeaveAlternateScreen)?;
1564
1565 Ok(())
1566 }
1567
1568 fn render_frame(
1569 &mut self,
1570 frame: &mut ratatui::Frame,
1571 show_throbber: bool,
1572 prompt_len: usize,
1573 indent_len: usize,
1574 ) {
1575 let frame_area = frame.area();
1576 let frame_width = frame_area.width as usize;
1577 let frame_height = frame_area.height;
1578 let theme = app_theme();
1579
1580 let ctx = self.build_layout_context(frame_area, show_throbber, prompt_len, indent_len, &theme);
1582 let sizes = self.compute_widget_sizes(frame_height);
1583 let layout = self.layout_template.compute(&ctx, &sizes);
1584
1585 let theme_picker_active = sizes.is_active(widget_ids::THEME_PICKER);
1587 let session_picker_active = sizes.is_active(widget_ids::SESSION_PICKER);
1588 let question_panel_active = sizes.is_active(widget_ids::QUESTION_PANEL);
1589 let permission_panel_active = sizes.is_active(widget_ids::PERMISSION_PANEL);
1590 let batch_permission_panel_active = sizes.is_active(widget_ids::BATCH_PERMISSION_PANEL);
1591
1592 let status_bar_data = StatusBarData {
1594 cwd: self.get_cwd(),
1595 model_name: self.model_name.clone(),
1596 context_used: self.context_used,
1597 context_limit: self.context_limit,
1598 session_id: self.session_id,
1599 status_hint: self.key_handler.status_hint(),
1600 is_waiting: show_throbber,
1601 waiting_elapsed: self.waiting_started.map(|t| t.elapsed()),
1602 input_empty: self.input().map(|i| i.is_empty()).unwrap_or(true),
1603 panels_active: question_panel_active || permission_panel_active || batch_permission_panel_active,
1604 };
1605
1606 if let Some(widget) = self.widgets.get_mut(widget_ids::STATUS_BAR) {
1608 if let Some(status_bar) = widget.as_any_mut().downcast_mut::<StatusBar>() {
1609 status_bar.update_data(status_bar_data);
1610 }
1611 }
1612
1613 for widget_id in &layout.render_order {
1615 if *widget_id == widget_ids::THEME_PICKER || *widget_id == widget_ids::SESSION_PICKER {
1617 continue;
1618 }
1619
1620 let Some(area) = layout.widget_areas.get(widget_id) else {
1622 continue;
1623 };
1624
1625 match *widget_id {
1627 id if id == widget_ids::CHAT_VIEW => {
1628 let pending_status: Option<&str> = if !self.executing_tools.is_empty() {
1630 Some(PENDING_STATUS_TOOLS)
1631 } else if self.waiting_for_response && !self.is_chat_streaming() {
1632 Some(PENDING_STATUS_LLM)
1633 } else {
1634 None
1635 };
1636 self.conversation_view.render(frame, *area, &theme, pending_status);
1637 }
1638 id if id == widget_ids::TEXT_INPUT => {
1639 }
1641 id if id == widget_ids::SLASH_POPUP => {
1642 if let Some(widget) = self.widgets.get(widget_ids::SLASH_POPUP) {
1643 if let Some(popup_state) = widget.as_any().downcast_ref::<SlashPopupState>() {
1644 let filtered: Vec<&dyn SlashCommand> = self.filtered_command_indices
1646 .iter()
1647 .filter_map(|&i| self.commands.get(i).map(|c| c.as_ref()))
1648 .collect();
1649 render_slash_popup(
1650 popup_state,
1651 &filtered,
1652 frame,
1653 *area,
1654 &theme,
1655 );
1656 }
1657 }
1658 }
1659 _ => {
1660 if let Some(widget) = self.widgets.get_mut(widget_id) {
1662 if widget.is_active() {
1663 widget.render(frame, *area, &theme);
1664 }
1665 }
1666 }
1667 }
1668 }
1669
1670 if let Some(input_area) = layout.input_area {
1672 if !question_panel_active && !permission_panel_active && !batch_permission_panel_active {
1673 if show_throbber {
1674 let default_message;
1675 let message = if let Some(msg) = &self.custom_throbber_message {
1676 msg.as_str()
1677 } else if let Some(ref msg_fn) = self.processing_message_fn {
1678 default_message = msg_fn();
1679 &default_message
1680 } else {
1681 &self.processing_message
1682 };
1683 let throbber = Throbber::default()
1684 .label(message)
1685 .style(theme.throbber_label)
1686 .throbber_style(theme.throbber_spinner)
1687 .throbber_set(BRAILLE_EIGHT_DOUBLE);
1688
1689 let throbber_block = Block::default()
1690 .borders(Borders::TOP | Borders::BOTTOM)
1691 .border_style(theme.input_border);
1692 let inner = throbber_block.inner(input_area);
1693 let throbber_inner = Rect::new(
1694 inner.x + 1,
1695 inner.y,
1696 inner.width.saturating_sub(1),
1697 inner.height,
1698 );
1699 frame.render_widget(throbber_block, input_area);
1700 frame.render_stateful_widget(throbber, throbber_inner, &mut self.throbber_state);
1701 } else if let Some(input) = self.input() {
1702 let input_lines: Vec<String> = input
1703 .buffer()
1704 .split('\n')
1705 .enumerate()
1706 .map(|(i, line)| {
1707 if i == 0 {
1708 format!("{}{}", PROMPT, line)
1709 } else {
1710 format!("{}{}", CONTINUATION_INDENT, line)
1711 }
1712 })
1713 .collect();
1714 let input_text = if input_lines.is_empty() {
1715 PROMPT.to_string()
1716 } else {
1717 input_lines.join("\n")
1718 };
1719
1720 let input_box = Paragraph::new(input_text)
1721 .block(
1722 Block::default()
1723 .borders(Borders::TOP | Borders::BOTTOM)
1724 .border_style(theme.input_border),
1725 )
1726 .wrap(Wrap { trim: false });
1727 frame.render_widget(input_box, input_area);
1728
1729 if !theme_picker_active && !session_picker_active {
1731 let (cursor_rel_x, cursor_rel_y) = input
1732 .cursor_display_position_wrapped(frame_width, prompt_len, indent_len);
1733 let cursor_x = input_area.x + cursor_rel_x;
1734 let cursor_y = input_area.y + 1 + cursor_rel_y;
1735 frame.set_cursor_position((cursor_x, cursor_y));
1736 }
1737 }
1738 }
1739 }
1740
1741
1742 if theme_picker_active {
1744 if let Some(widget) = self.widgets.get(widget_ids::THEME_PICKER) {
1745 if let Some(picker) = widget.as_any().downcast_ref::<ThemePickerState>() {
1746 render_theme_picker(picker, frame, frame_area);
1747 }
1748 }
1749 }
1750
1751 if session_picker_active {
1752 if let Some(widget) = self.widgets.get(widget_ids::SESSION_PICKER) {
1753 if let Some(picker) = widget.as_any().downcast_ref::<SessionPickerState>() {
1754 render_session_picker(picker, frame, frame_area, &theme);
1755 }
1756 }
1757 }
1758 }
1759}
1760
1761impl Default for App {
1762 fn default() -> Self {
1763 Self::new()
1764 }
1765}