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, PermissionRegistry, PermissionResponse,
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,
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: PermissionResponse) {
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(&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 process_controller_messages(&mut self) {
905 let mut messages = Vec::new();
907
908 if let Some(ref mut rx) = self.from_controller {
909 loop {
910 match rx.try_recv() {
911 Ok(msg) => messages.push(msg),
912 Err(mpsc::error::TryRecvError::Empty) => break,
913 Err(mpsc::error::TryRecvError::Disconnected) => {
914 tracing::warn!("Controller channel disconnected");
915 break;
916 }
917 }
918 }
919 }
920
921 for msg in messages {
923 self.handle_ui_message(msg);
924 }
925 }
926
927 fn handle_ui_message(&mut self, msg: UiMessage) {
929 match msg {
930 UiMessage::TextChunk { text, turn_id, .. } => {
931 if !self.is_current_turn(&turn_id) {
933 return;
934 }
935 self.conversation_view.append_streaming(&text);
936 }
937 UiMessage::Display { message, .. } => {
938 self.conversation_view.add_system_message(message);
939 }
940 UiMessage::Complete {
941 turn_id,
942 stop_reason,
943 ..
944 } => {
945 if !self.is_current_turn(&turn_id) {
947 return;
948 }
949
950 let is_tool_use = stop_reason.as_deref() == Some("tool_use");
952
953 self.conversation_view.complete_streaming();
954
955 if !is_tool_use {
957 self.waiting_for_response = false;
958 self.waiting_started = None;
959 }
960 }
961 UiMessage::TokenUpdate {
962 input_tokens,
963 context_limit,
964 ..
965 } => {
966 self.context_used = input_tokens;
967 self.context_limit = context_limit;
968 }
969 UiMessage::Error { error, turn_id, .. } => {
970 if !self.is_current_turn(&turn_id) {
971 return;
972 }
973 self.conversation_view.complete_streaming();
974 self.waiting_for_response = false;
975 self.waiting_started = None;
976 self.current_turn_id = None;
977 self.conversation_view.add_system_message(format!("Error: {}", error));
978 }
979 UiMessage::System { message, .. } => {
980 self.conversation_view.add_system_message(message);
981 }
982 UiMessage::ToolExecuting {
983 tool_use_id,
984 display_name,
985 display_title,
986 ..
987 } => {
988 self.executing_tools.insert(tool_use_id.clone());
989 self.conversation_view.add_tool_message(&tool_use_id, &display_name, &display_title);
990 }
991 UiMessage::ToolCompleted {
992 tool_use_id,
993 status,
994 error,
995 ..
996 } => {
997 self.executing_tools.remove(&tool_use_id);
998 let tool_status = if status == ToolResultStatus::Success {
999 ToolStatus::Completed
1000 } else {
1001 ToolStatus::Failed(error.unwrap_or_default())
1002 };
1003 self.conversation_view.update_tool_status(&tool_use_id, tool_status);
1004 }
1005 UiMessage::CommandComplete {
1006 command,
1007 success,
1008 message,
1009 ..
1010 } => {
1011 self.waiting_for_response = false;
1012 self.waiting_started = None;
1013 self.custom_throbber_message = None;
1014
1015 match command {
1016 ControlCmd::Compact => {
1017 if let Some(msg) = message {
1018 self.conversation_view.add_system_message(msg);
1019 }
1020 }
1021 ControlCmd::Clear => {}
1022 _ => {
1023 tracing::debug!(?command, ?success, "Command completed");
1024 }
1025 }
1026 }
1027 UiMessage::UserInteractionRequired {
1028 session_id,
1029 tool_use_id,
1030 request,
1031 turn_id,
1032 } => {
1033 if session_id == self.session_id {
1034 self.conversation_view.update_tool_status(&tool_use_id, ToolStatus::WaitingForUser);
1035 if let Some(widget) = self.widgets.get_mut(widget_ids::QUESTION_PANEL) {
1037 if let Some(panel) = widget.as_any_mut().downcast_mut::<QuestionPanel>() {
1038 panel.activate(tool_use_id, session_id, request, turn_id);
1039 }
1040 }
1041 }
1042 }
1043 UiMessage::PermissionRequired {
1044 session_id,
1045 tool_use_id,
1046 request,
1047 turn_id,
1048 } => {
1049 if session_id == self.session_id {
1050 self.conversation_view.update_tool_status(&tool_use_id, ToolStatus::WaitingForUser);
1051 if let Some(widget) = self.widgets.get_mut(widget_ids::PERMISSION_PANEL) {
1053 if let Some(panel) = widget.as_any_mut().downcast_mut::<PermissionPanel>() {
1054 panel.activate(tool_use_id, session_id, request, turn_id);
1055 }
1056 }
1057 }
1058 }
1059 }
1060 }
1061
1062 fn is_current_turn(&self, turn_id: &Option<TurnId>) -> bool {
1064 match (&self.current_turn_id, turn_id) {
1065 (Some(current), Some(incoming)) => current == incoming,
1066 (None, _) => false,
1067 (Some(_), None) => false,
1068 }
1069 }
1070
1071 pub fn scroll_up(&mut self) {
1072 self.conversation_view.scroll_up();
1073 }
1074
1075 pub fn scroll_down(&mut self) {
1076 self.conversation_view.scroll_down();
1077 }
1078
1079 fn get_cwd(&self) -> String {
1081 std::env::current_dir()
1082 .map(|p| {
1083 let path_str = p.display().to_string();
1084 if let Some(home) = std::env::var_os("HOME") {
1085 let home_str = home.to_string_lossy();
1086 if path_str.starts_with(home_str.as_ref()) {
1087 return format!("~{}", &path_str[home_str.len()..]);
1088 }
1089 }
1090 path_str
1091 })
1092 .unwrap_or_else(|_| "unknown".to_string())
1093 }
1094
1095 fn handle_key(&mut self, key: KeyCode, modifiers: KeyModifiers) {
1097 let key_event = KeyEvent::new(key, modifiers);
1099 let context = KeyContext {
1100 input_empty: self.input().map(|i| i.is_empty()).unwrap_or(true),
1101 is_processing: self.waiting_for_response || self.is_chat_streaming(),
1102 widget_blocking: self.any_widget_blocks_input(),
1103 };
1104
1105 let result = self.key_handler.handle_key(key_event, &context);
1108
1109 match result {
1110 AppKeyResult::Handled => return,
1111 AppKeyResult::Action(action) => {
1112 self.execute_key_action(action);
1113 return;
1114 }
1115 AppKeyResult::NotHandled => {
1116 }
1118 }
1119
1120 let theme = app_theme();
1122 let nav = NavigationHelper::new(self.key_handler.bindings());
1123 let widget_ctx = WidgetKeyContext { theme: &theme, nav };
1124
1125 let widget_ids_to_check: Vec<&'static str> = self.widget_priority_order.clone();
1127
1128 for widget_id in widget_ids_to_check {
1129 if let Some(widget) = self.widgets.get_mut(widget_id) {
1130 if widget.is_active() {
1131 match widget.handle_key(key_event, &widget_ctx) {
1132 WidgetKeyResult::Handled => return,
1133 WidgetKeyResult::Action(action) => {
1134 self.process_widget_action(action);
1135 return;
1136 }
1137 WidgetKeyResult::NotHandled => {
1138 }
1140 }
1141 }
1142 }
1143 }
1144
1145 if self.is_slash_popup_active() {
1147 self.handle_slash_popup_key(key);
1148 return;
1149 }
1150 }
1151
1152 fn execute_key_action(&mut self, action: AppKeyAction) {
1154 match action {
1155 AppKeyAction::MoveUp => {
1156 if let Some(input) = self.input_mut() {
1157 input.move_up();
1158 }
1159 }
1160 AppKeyAction::MoveDown => {
1161 if let Some(input) = self.input_mut() {
1162 input.move_down();
1163 }
1164 }
1165 AppKeyAction::MoveLeft => {
1166 if let Some(input) = self.input_mut() {
1167 input.move_left();
1168 }
1169 }
1170 AppKeyAction::MoveRight => {
1171 if let Some(input) = self.input_mut() {
1172 input.move_right();
1173 }
1174 }
1175 AppKeyAction::MoveLineStart => {
1176 if let Some(input) = self.input_mut() {
1177 input.move_to_line_start();
1178 }
1179 }
1180 AppKeyAction::MoveLineEnd => {
1181 if let Some(input) = self.input_mut() {
1182 input.move_to_line_end();
1183 }
1184 }
1185 AppKeyAction::DeleteCharBefore => {
1186 if let Some(input) = self.input_mut() {
1187 input.delete_char_before();
1188 }
1189 }
1190 AppKeyAction::DeleteCharAt => {
1191 if let Some(input) = self.input_mut() {
1192 input.delete_char_at();
1193 }
1194 }
1195 AppKeyAction::KillLine => {
1196 if let Some(input) = self.input_mut() {
1197 input.kill_line();
1198 }
1199 }
1200 AppKeyAction::InsertNewline => {
1201 if let Some(input) = self.input_mut() {
1202 input.insert_char('\n');
1203 }
1204 }
1205 AppKeyAction::InsertChar(c) => {
1206 if let Some(input) = self.input_mut() {
1207 input.insert_char(c);
1208 }
1209 if c == '/' && self.input().map(|i| i.buffer() == "/").unwrap_or(false) {
1211 self.activate_slash_popup();
1212 }
1213 }
1214 AppKeyAction::Submit => {
1215 self.submit_message();
1216 }
1217 AppKeyAction::Interrupt => {
1218 self.interrupt_request();
1219 }
1220 AppKeyAction::Quit => {
1221 self.should_quit = true;
1222 }
1223 AppKeyAction::RequestExit => {
1224 let should_quit = self.exit_handler
1226 .as_mut()
1227 .map(|h| h.on_exit())
1228 .unwrap_or(true);
1229 if should_quit {
1230 self.should_quit = true;
1231 }
1232 }
1233 AppKeyAction::ActivateSlashPopup => {
1234 self.activate_slash_popup();
1235 }
1236 AppKeyAction::Custom(_) => {
1237 }
1241 }
1242 }
1243
1244 fn process_widget_action(&mut self, action: WidgetAction) {
1246 match action {
1247 WidgetAction::SubmitQuestion { tool_use_id, response } => {
1248 self.submit_question_panel_response(tool_use_id, response);
1249 }
1250 WidgetAction::CancelQuestion { tool_use_id } => {
1251 self.cancel_question_panel_response(tool_use_id);
1252 }
1253 WidgetAction::SubmitPermission { tool_use_id, response } => {
1254 self.submit_permission_panel_response(tool_use_id, response);
1255 }
1256 WidgetAction::CancelPermission { tool_use_id } => {
1257 self.cancel_permission_panel_response(tool_use_id);
1258 }
1259 WidgetAction::SwitchSession { session_id } => {
1260 self.switch_session(session_id);
1261 }
1262 WidgetAction::ExecuteCommand { command } => {
1263 if command.starts_with("__SLASH_INDEX_") {
1265 if let Ok(idx) = command.trim_start_matches("__SLASH_INDEX_").parse::<usize>() {
1266 self.execute_slash_command_at_index(idx);
1267 }
1268 } else {
1269 self.execute_command(&command);
1270 }
1271 }
1272 WidgetAction::Close => {
1273 }
1275 }
1276 }
1277
1278 fn is_slash_popup_active(&self) -> bool {
1280 self.widgets
1281 .get(widget_ids::SLASH_POPUP)
1282 .map(|w| w.is_active())
1283 .unwrap_or(false)
1284 }
1285
1286 fn filter_command_indices(&self, prefix: &str) -> Vec<usize> {
1288 let search_term = prefix.trim_start_matches('/').to_lowercase();
1289 self.commands
1290 .iter()
1291 .enumerate()
1292 .filter(|(_, cmd)| cmd.name().to_lowercase().starts_with(&search_term))
1293 .map(|(i, _)| i)
1294 .collect()
1295 }
1296
1297 fn activate_slash_popup(&mut self) {
1299 let indices = self.filter_command_indices("/");
1301 let count = indices.len();
1302 self.filtered_command_indices = indices;
1303
1304 if let Some(widget) = self.widgets.get_mut(widget_ids::SLASH_POPUP) {
1305 if let Some(popup) = widget.as_any_mut().downcast_mut::<SlashPopupState>() {
1306 popup.activate();
1307 popup.set_filtered_count(count);
1308 }
1309 }
1310 }
1311
1312 fn handle_slash_popup_key(&mut self, key: KeyCode) {
1314 match key {
1315 KeyCode::Up => {
1316 if let Some(widget) = self.widgets.get_mut(widget_ids::SLASH_POPUP) {
1317 if let Some(popup) = widget.as_any_mut().downcast_mut::<SlashPopupState>() {
1318 popup.select_previous();
1319 }
1320 }
1321 }
1322 KeyCode::Down => {
1323 if let Some(widget) = self.widgets.get_mut(widget_ids::SLASH_POPUP) {
1324 if let Some(popup) = widget.as_any_mut().downcast_mut::<SlashPopupState>() {
1325 popup.select_next();
1326 }
1327 }
1328 }
1329 KeyCode::Enter => {
1330 let selected_idx = self.widgets
1331 .get(widget_ids::SLASH_POPUP)
1332 .and_then(|w| w.as_any().downcast_ref::<SlashPopupState>())
1333 .map(|p| p.selected_index)
1334 .unwrap_or(0);
1335 self.execute_slash_command_at_index(selected_idx);
1336 }
1337 KeyCode::Esc => {
1338 if let Some(input) = self.input_mut() {
1339 input.clear();
1340 }
1341 if let Some(widget) = self.widgets.get_mut(widget_ids::SLASH_POPUP) {
1342 if let Some(popup) = widget.as_any_mut().downcast_mut::<SlashPopupState>() {
1343 popup.deactivate();
1344 }
1345 }
1346 self.filtered_command_indices.clear();
1347 }
1348 KeyCode::Backspace => {
1349 let is_just_slash = self.input().map(|i| i.buffer() == "/").unwrap_or(false);
1350 if is_just_slash {
1351 if let Some(input) = self.input_mut() {
1352 input.clear();
1353 }
1354 if let Some(widget) = self.widgets.get_mut(widget_ids::SLASH_POPUP) {
1355 if let Some(popup) = widget.as_any_mut().downcast_mut::<SlashPopupState>() {
1356 popup.deactivate();
1357 }
1358 }
1359 self.filtered_command_indices.clear();
1360 } else {
1361 if let Some(input) = self.input_mut() {
1362 input.delete_char_before();
1363 }
1364 let buffer = self.input().map(|i| i.buffer().to_string()).unwrap_or_default();
1365 self.filtered_command_indices = self.filter_command_indices(&buffer);
1366 if let Some(widget) = self.widgets.get_mut(widget_ids::SLASH_POPUP) {
1367 if let Some(popup) = widget.as_any_mut().downcast_mut::<SlashPopupState>() {
1368 popup.set_filtered_count(self.filtered_command_indices.len());
1369 }
1370 }
1371 }
1372 }
1373 KeyCode::Char(c) => {
1374 if let Some(input) = self.input_mut() {
1375 input.insert_char(c);
1376 }
1377 let buffer = self.input().map(|i| i.buffer().to_string()).unwrap_or_default();
1378 self.filtered_command_indices = self.filter_command_indices(&buffer);
1379 if let Some(widget) = self.widgets.get_mut(widget_ids::SLASH_POPUP) {
1380 if let Some(popup) = widget.as_any_mut().downcast_mut::<SlashPopupState>() {
1381 popup.set_filtered_count(self.filtered_command_indices.len());
1382 }
1383 }
1384 }
1385 _ => {
1386 if let Some(widget) = self.widgets.get_mut(widget_ids::SLASH_POPUP) {
1387 if let Some(popup) = widget.as_any_mut().downcast_mut::<SlashPopupState>() {
1388 popup.deactivate();
1389 }
1390 }
1391 }
1392 }
1393 }
1394
1395 fn execute_slash_command_at_index(&mut self, idx: usize) {
1397 if let Some(&cmd_idx) = self.filtered_command_indices.get(idx) {
1399 if let Some(cmd) = self.commands.get(cmd_idx) {
1400 let cmd_name = cmd.name().to_string();
1401 if let Some(input) = self.input_mut() {
1402 input.clear();
1403 for c in format!("/{}", cmd_name).chars() {
1404 input.insert_char(c);
1405 }
1406 }
1407 if let Some(widget) = self.widgets.get_mut(widget_ids::SLASH_POPUP) {
1408 if let Some(popup) = widget.as_any_mut().downcast_mut::<SlashPopupState>() {
1409 popup.deactivate();
1410 }
1411 }
1412 self.filtered_command_indices.clear();
1413 self.submit_message();
1414 }
1415 }
1416 }
1417
1418 pub fn run(&mut self) -> io::Result<()> {
1419 enable_raw_mode()?;
1420 io::stdout().execute(EnterAlternateScreen)?;
1421 io::stdout().execute(EnableMouseCapture)?;
1422
1423 let mut terminal = Terminal::new(CrosstermBackend::new(io::stdout()))?;
1424
1425 while !self.should_quit {
1426 self.process_controller_messages();
1427
1428 let show_throbber = self.waiting_for_response
1429 || self.is_chat_streaming()
1430 || !self.executing_tools.is_empty();
1431
1432 if show_throbber {
1434 self.animation_frame_counter = self.animation_frame_counter.wrapping_add(1);
1435 if self.animation_frame_counter % 6 == 0 {
1436 self.throbber_state.calc_next();
1437 self.conversation_view.step_spinner();
1438 }
1439 }
1440
1441 let prompt_len = PROMPT.chars().count();
1442 let indent_len = CONTINUATION_INDENT.len();
1443
1444 terminal.draw(|frame| {
1445 self.render_frame(frame, show_throbber, prompt_len, indent_len);
1446 })?;
1447
1448 let mut net_scroll: i32 = 0;
1450
1451 while event::poll(std::time::Duration::from_millis(0))? {
1452 match event::read()? {
1453 Event::Key(key) => {
1454 if key.kind == KeyEventKind::Press {
1455 self.handle_key(key.code, key.modifiers);
1456 }
1457 }
1458 Event::Mouse(mouse) => match mouse.kind {
1459 MouseEventKind::ScrollUp => net_scroll -= 1,
1460 MouseEventKind::ScrollDown => net_scroll += 1,
1461 _ => {}
1462 },
1463 _ => {}
1464 }
1465 }
1466
1467 if net_scroll < 0 {
1469 for _ in 0..(-net_scroll) {
1470 self.scroll_up();
1471 }
1472 } else if net_scroll > 0 {
1473 for _ in 0..net_scroll {
1474 self.scroll_down();
1475 }
1476 }
1477
1478 if net_scroll == 0 {
1479 std::thread::sleep(std::time::Duration::from_millis(16));
1480 }
1481 }
1482
1483 io::stdout().execute(DisableMouseCapture)?;
1484 disable_raw_mode()?;
1485 io::stdout().execute(LeaveAlternateScreen)?;
1486
1487 Ok(())
1488 }
1489
1490 fn render_frame(
1491 &mut self,
1492 frame: &mut ratatui::Frame,
1493 show_throbber: bool,
1494 prompt_len: usize,
1495 indent_len: usize,
1496 ) {
1497 let frame_area = frame.area();
1498 let frame_width = frame_area.width as usize;
1499 let frame_height = frame_area.height;
1500 let theme = app_theme();
1501
1502 let ctx = self.build_layout_context(frame_area, show_throbber, prompt_len, indent_len, &theme);
1504 let sizes = self.compute_widget_sizes(frame_height);
1505 let layout = self.layout_template.compute(&ctx, &sizes);
1506
1507 let theme_picker_active = sizes.is_active(widget_ids::THEME_PICKER);
1509 let session_picker_active = sizes.is_active(widget_ids::SESSION_PICKER);
1510 let question_panel_active = sizes.is_active(widget_ids::QUESTION_PANEL);
1511 let permission_panel_active = sizes.is_active(widget_ids::PERMISSION_PANEL);
1512
1513 let status_bar_data = StatusBarData {
1515 cwd: self.get_cwd(),
1516 model_name: self.model_name.clone(),
1517 context_used: self.context_used,
1518 context_limit: self.context_limit,
1519 session_id: self.session_id,
1520 status_hint: self.key_handler.status_hint(),
1521 is_waiting: show_throbber,
1522 waiting_elapsed: self.waiting_started.map(|t| t.elapsed()),
1523 input_empty: self.input().map(|i| i.is_empty()).unwrap_or(true),
1524 panels_active: question_panel_active || permission_panel_active,
1525 };
1526
1527 if let Some(widget) = self.widgets.get_mut(widget_ids::STATUS_BAR) {
1529 if let Some(status_bar) = widget.as_any_mut().downcast_mut::<StatusBar>() {
1530 status_bar.update_data(status_bar_data);
1531 }
1532 }
1533
1534 for widget_id in &layout.render_order {
1536 if *widget_id == widget_ids::THEME_PICKER || *widget_id == widget_ids::SESSION_PICKER {
1538 continue;
1539 }
1540
1541 let Some(area) = layout.widget_areas.get(widget_id) else {
1543 continue;
1544 };
1545
1546 match *widget_id {
1548 id if id == widget_ids::CHAT_VIEW => {
1549 let pending_status: Option<&str> = if !self.executing_tools.is_empty() {
1551 Some(PENDING_STATUS_TOOLS)
1552 } else if self.waiting_for_response && !self.is_chat_streaming() {
1553 Some(PENDING_STATUS_LLM)
1554 } else {
1555 None
1556 };
1557 self.conversation_view.render(frame, *area, &theme, pending_status);
1558 }
1559 id if id == widget_ids::TEXT_INPUT => {
1560 }
1562 id if id == widget_ids::SLASH_POPUP => {
1563 if let Some(widget) = self.widgets.get(widget_ids::SLASH_POPUP) {
1564 if let Some(popup_state) = widget.as_any().downcast_ref::<SlashPopupState>() {
1565 let filtered: Vec<&dyn SlashCommand> = self.filtered_command_indices
1567 .iter()
1568 .filter_map(|&i| self.commands.get(i).map(|c| c.as_ref()))
1569 .collect();
1570 render_slash_popup(
1571 popup_state,
1572 &filtered,
1573 frame,
1574 *area,
1575 &theme,
1576 );
1577 }
1578 }
1579 }
1580 _ => {
1581 if let Some(widget) = self.widgets.get_mut(widget_id) {
1583 if widget.is_active() {
1584 widget.render(frame, *area, &theme);
1585 }
1586 }
1587 }
1588 }
1589 }
1590
1591 if let Some(input_area) = layout.input_area {
1593 if !question_panel_active && !permission_panel_active {
1594 if show_throbber {
1595 let default_message;
1596 let message = if let Some(msg) = &self.custom_throbber_message {
1597 msg.as_str()
1598 } else if let Some(ref msg_fn) = self.processing_message_fn {
1599 default_message = msg_fn();
1600 &default_message
1601 } else {
1602 &self.processing_message
1603 };
1604 let throbber = Throbber::default()
1605 .label(message)
1606 .style(theme.throbber_label)
1607 .throbber_style(theme.throbber_spinner)
1608 .throbber_set(BRAILLE_EIGHT_DOUBLE);
1609
1610 let throbber_block = Block::default()
1611 .borders(Borders::TOP | Borders::BOTTOM)
1612 .border_style(theme.input_border);
1613 let inner = throbber_block.inner(input_area);
1614 let throbber_inner = Rect::new(
1615 inner.x + 1,
1616 inner.y,
1617 inner.width.saturating_sub(1),
1618 inner.height,
1619 );
1620 frame.render_widget(throbber_block, input_area);
1621 frame.render_stateful_widget(throbber, throbber_inner, &mut self.throbber_state);
1622 } else if let Some(input) = self.input() {
1623 let input_lines: Vec<String> = input
1624 .buffer()
1625 .split('\n')
1626 .enumerate()
1627 .map(|(i, line)| {
1628 if i == 0 {
1629 format!("{}{}", PROMPT, line)
1630 } else {
1631 format!("{}{}", CONTINUATION_INDENT, line)
1632 }
1633 })
1634 .collect();
1635 let input_text = if input_lines.is_empty() {
1636 PROMPT.to_string()
1637 } else {
1638 input_lines.join("\n")
1639 };
1640
1641 let input_box = Paragraph::new(input_text)
1642 .block(
1643 Block::default()
1644 .borders(Borders::TOP | Borders::BOTTOM)
1645 .border_style(theme.input_border),
1646 )
1647 .wrap(Wrap { trim: false });
1648 frame.render_widget(input_box, input_area);
1649
1650 if !theme_picker_active && !session_picker_active {
1652 let (cursor_rel_x, cursor_rel_y) = input
1653 .cursor_display_position_wrapped(frame_width, prompt_len, indent_len);
1654 let cursor_x = input_area.x + cursor_rel_x;
1655 let cursor_y = input_area.y + 1 + cursor_rel_y;
1656 frame.set_cursor_position((cursor_x, cursor_y));
1657 }
1658 }
1659 }
1660 }
1661
1662
1663 if theme_picker_active {
1665 if let Some(widget) = self.widgets.get(widget_ids::THEME_PICKER) {
1666 if let Some(picker) = widget.as_any().downcast_ref::<ThemePickerState>() {
1667 render_theme_picker(picker, frame, frame_area);
1668 }
1669 }
1670 }
1671
1672 if session_picker_active {
1673 if let Some(widget) = self.widgets.get(widget_ids::SESSION_PICKER) {
1674 if let Some(picker) = widget.as_any().downcast_ref::<SessionPickerState>() {
1675 render_session_picker(picker, frame, frame_area, &theme);
1676 }
1677 }
1678 }
1679 }
1680}
1681
1682impl Default for App {
1683 fn default() -> Self {
1684 Self::new()
1685 }
1686}