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}
82
83impl std::fmt::Debug for AppConfig {
84 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85 f.debug_struct("AppConfig")
86 .field("agent_name", &self.agent_name)
87 .field("version", &self.version)
88 .field("commands", &self.commands.as_ref().map(|c| format!("<{} commands>", c.len())))
89 .field("command_extension", &self.command_extension.as_ref().map(|_| "<extension>"))
90 .field("processing_message", &self.processing_message)
91 .field("processing_message_fn", &self.processing_message_fn.as_ref().map(|_| "<fn>"))
92 .finish()
93 }
94}
95
96impl Default for AppConfig {
97 fn default() -> Self {
98 Self {
99 agent_name: "Agent".to_string(),
100 version: "0.1.0".to_string(),
101 commands: None, command_extension: None,
103 processing_message: "Processing request...".to_string(),
104 processing_message_fn: None,
105 }
106 }
107}
108
109pub struct App {
113 agent_name: String,
115
116 version: String,
118
119 commands: Vec<Box<dyn SlashCommand>>,
121
122 command_extension: Option<Box<dyn std::any::Any + Send>>,
124
125 processing_message: String,
127
128 processing_message_fn: Option<ProcessingMessageFn>,
130
131 pub should_quit: bool,
133
134 to_controller: Option<ToControllerTx>,
136
137 from_controller: Option<FromControllerRx>,
139
140 controller: Option<Arc<LLMController>>,
142
143 llm_registry: Option<LLMRegistry>,
145
146 runtime_handle: Option<Handle>,
148
149 session_id: i64,
151
152 user_turn_counter: i64,
154
155 model_name: String,
157
158 context_used: i64,
160
161 context_limit: i32,
163
164 throbber_state: ThrobberState,
166
167 pub waiting_for_response: bool,
169
170 waiting_started: Option<Instant>,
172
173 animation_frame_counter: u8,
175
176 current_turn_id: Option<TurnId>,
178
179 executing_tools: HashSet<String>,
181
182 pub widgets: HashMap<&'static str, Box<dyn Widget>>,
184
185 pub widget_priority_order: Vec<&'static str>,
187
188 filtered_command_indices: Vec<usize>,
190
191 sessions: Vec<SessionInfo>,
193
194 session_states: HashMap<i64, Box<dyn std::any::Any + Send>>,
196
197 conversation_view: Box<dyn ConversationView>,
199
200 conversation_factory: ConversationViewFactory,
202
203 custom_throbber_message: Option<String>,
205
206 user_interaction_registry: Option<Arc<UserInteractionRegistry>>,
208
209 permission_registry: Option<Arc<PermissionRegistry>>,
211
212 layout_template: LayoutTemplate,
214
215 key_handler: Box<dyn KeyHandler>,
217
218 exit_handler: Option<Box<dyn ExitHandler>>,
220}
221
222impl App {
223 pub fn new() -> Self {
225 Self::with_config(AppConfig::default())
226 }
227
228 pub fn with_config(config: AppConfig) -> Self {
230 use super::commands::default_commands;
231
232 let theme_name = default_theme_name();
234 if let Some(theme) = get_theme(theme_name) {
235 init_theme(theme_name, theme);
236 }
237
238 let commands = config.commands.unwrap_or_else(default_commands);
240
241 let default_factory: ConversationViewFactory = Box::new(|| {
243 Box::new(ChatView::new())
244 });
245
246 let mut app = Self {
247 agent_name: config.agent_name,
248 version: config.version,
249 commands,
250 command_extension: config.command_extension,
251 processing_message: config.processing_message,
252 processing_message_fn: config.processing_message_fn,
253 should_quit: false,
254 to_controller: None,
255 from_controller: None,
256 controller: None,
257 llm_registry: None,
258 runtime_handle: None,
259 session_id: 0,
260 user_turn_counter: 0,
261 model_name: "Not connected".to_string(),
262 context_used: 0,
263 context_limit: 0,
264 throbber_state: ThrobberState::default(),
265 waiting_for_response: false,
266 waiting_started: None,
267 animation_frame_counter: 0,
268 current_turn_id: None,
269 executing_tools: HashSet::new(),
270 widgets: HashMap::new(),
271 widget_priority_order: Vec::new(),
272 filtered_command_indices: Vec::new(),
273 sessions: Vec::new(),
274 session_states: HashMap::new(),
275 conversation_view: (default_factory)(),
276 conversation_factory: default_factory,
277 custom_throbber_message: None,
278 user_interaction_registry: None,
279 permission_registry: None,
280 layout_template: LayoutTemplate::default(),
281 key_handler: Box::new(DefaultKeyHandler::default()),
282 exit_handler: None,
283 };
284
285 app.register_widget(StatusBar::new());
287
288 app
289 }
290
291 pub fn register_widget<W: Widget>(&mut self, widget: W) {
296 let id = widget.id();
297 self.widgets.insert(id, Box::new(widget));
298 self.rebuild_priority_order();
299 }
300
301 pub fn rebuild_priority_order(&mut self) {
303 let mut order: Vec<_> = self.widgets.keys().copied().collect();
304 order.sort_by(|a, b| {
305 let priority_a = self.widgets.get(a).map(|w| w.priority()).unwrap_or(0);
306 let priority_b = self.widgets.get(b).map(|w| w.priority()).unwrap_or(0);
307 priority_b.cmp(&priority_a) });
309 self.widget_priority_order = order;
310 }
311
312 pub fn widget<W: Widget + 'static>(&self, id: &str) -> Option<&W> {
314 self.widgets.get(id).and_then(|w| w.as_any().downcast_ref::<W>())
315 }
316
317 pub fn widget_mut<W: Widget + 'static>(&mut self, id: &str) -> Option<&mut W> {
319 self.widgets.get_mut(id).and_then(|w| w.as_any_mut().downcast_mut::<W>())
320 }
321
322 pub fn has_widget(&self, id: &str) -> bool {
324 self.widgets.contains_key(id)
325 }
326
327 fn any_widget_blocks_input(&self) -> bool {
329 self.widgets.values().any(|w| w.is_active() && w.blocks_input())
330 }
331
332 pub fn set_conversation_factory<F>(&mut self, factory: F)
348 where
349 F: Fn() -> Box<dyn ConversationView> + Send + Sync + 'static,
350 {
351 self.conversation_factory = Box::new(factory);
352 self.conversation_view = (self.conversation_factory)();
354 }
355
356 fn input(&self) -> Option<&TextInput> {
358 self.widget::<TextInput>(widget_ids::TEXT_INPUT)
359 }
360
361 fn input_mut(&mut self) -> Option<&mut TextInput> {
363 self.widget_mut::<TextInput>(widget_ids::TEXT_INPUT)
364 }
365
366 fn is_chat_streaming(&self) -> bool {
368 self.conversation_view.is_streaming()
369 }
370
371 pub fn agent_name(&self) -> &str {
373 &self.agent_name
374 }
375
376 pub fn version(&self) -> &str {
378 &self.version
379 }
380
381 pub fn set_to_controller(&mut self, tx: ToControllerTx) {
383 self.to_controller = Some(tx);
384 }
385
386 pub fn set_from_controller(&mut self, rx: FromControllerRx) {
388 self.from_controller = Some(rx);
389 }
390
391 pub fn set_controller(&mut self, controller: Arc<LLMController>) {
393 self.controller = Some(controller);
394 }
395
396 pub fn set_llm_registry(&mut self, registry: LLMRegistry) {
398 self.llm_registry = Some(registry);
399 }
400
401 pub fn set_runtime_handle(&mut self, handle: Handle) {
403 self.runtime_handle = Some(handle);
404 }
405
406 pub fn set_user_interaction_registry(&mut self, registry: Arc<UserInteractionRegistry>) {
408 self.user_interaction_registry = Some(registry);
409 }
410
411 pub fn set_permission_registry(&mut self, registry: Arc<PermissionRegistry>) {
413 self.permission_registry = Some(registry);
414 }
415
416 pub fn set_session_id(&mut self, id: i64) {
418 self.session_id = id;
419 }
420
421 pub fn set_model_name(&mut self, name: impl Into<String>) {
423 self.model_name = name.into();
424 }
425
426 pub fn set_context_limit(&mut self, limit: i32) {
428 self.context_limit = limit;
429 }
430
431 pub fn set_layout(&mut self, template: LayoutTemplate) {
433 self.layout_template = template;
434 }
435
436 pub fn set_key_handler<H: KeyHandler>(&mut self, handler: H) {
441 self.key_handler = Box::new(handler);
442 }
443
444 pub fn set_key_handler_boxed(&mut self, handler: Box<dyn KeyHandler>) {
448 self.key_handler = handler;
449 }
450
451 pub fn set_key_bindings(&mut self, bindings: KeyBindings) {
456 self.key_handler = Box::new(DefaultKeyHandler::new(bindings));
457 }
458
459 pub fn set_exit_handler<H: ExitHandler>(&mut self, handler: H) {
464 self.exit_handler = Some(Box::new(handler));
465 }
466
467 pub fn set_exit_handler_boxed(&mut self, handler: Box<dyn ExitHandler>) {
469 self.exit_handler = Some(handler);
470 }
471
472 fn compute_widget_sizes(&self, frame_height: u16) -> WidgetSizes {
474 let mut heights = HashMap::new();
475 let mut is_active = HashMap::new();
476
477 for (id, widget) in &self.widgets {
478 heights.insert(*id, widget.required_height(frame_height));
479 is_active.insert(*id, widget.is_active());
480 }
481
482 WidgetSizes { heights, is_active }
483 }
484
485 fn build_layout_context<'a>(
487 &self,
488 frame_area: Rect,
489 show_throbber: bool,
490 prompt_len: usize,
491 indent_len: usize,
492 theme: &'a super::themes::Theme,
493 ) -> LayoutContext<'a> {
494 let frame_width = frame_area.width as usize;
495 let input_visual_lines = self
496 .input()
497 .map(|i| i.visual_line_count(frame_width, prompt_len, indent_len))
498 .unwrap_or(1);
499
500 let mut active_widgets = HashSet::new();
501 for (id, widget) in &self.widgets {
502 if widget.is_active() {
503 active_widgets.insert(*id);
504 }
505 }
506
507 LayoutContext {
508 frame_area,
509 show_throbber,
510 input_visual_lines,
511 theme,
512 active_widgets,
513 }
514 }
515
516 pub fn submit_message(&mut self) {
517 let content = match self.input_mut() {
519 Some(input) => input.take(),
520 None => return, };
522 if content.trim().is_empty() {
523 return;
524 }
525
526 if is_slash_command(&content) {
528 self.execute_command(&content);
529 return;
530 }
531
532 self.conversation_view.enable_auto_scroll();
534 self.conversation_view.add_user_message(content.clone());
535
536 if self.session_id == 0 {
538 self.conversation_view.add_system_message(
539 "No active session. Use /new-session to create one.".to_string(),
540 );
541 return;
542 }
543
544 if let Some(ref tx) = self.to_controller {
546 self.user_turn_counter += 1;
547 let turn_id = TurnId::new_user_turn(self.user_turn_counter);
548 let payload = ControllerInputPayload::data(self.session_id, content, turn_id);
549
550 if tx.try_send(payload).is_err() {
552 self.conversation_view.add_system_message("Failed to send message to controller".to_string());
553 } else {
554 self.waiting_for_response = true;
556 self.waiting_started = Some(Instant::now());
557 self.current_turn_id = Some(TurnId::new_user_turn(self.user_turn_counter));
559 }
560 }
561 }
562
563 pub fn interrupt_request(&mut self) {
565 if !self.waiting_for_response
567 && !self.is_chat_streaming()
568 && self.executing_tools.is_empty()
569 {
570 return;
571 }
572
573 if let Some(ref tx) = self.to_controller {
575 let payload = ControllerInputPayload::control(self.session_id, ControlCmd::Interrupt);
576 if tx.try_send(payload).is_ok() {
577 self.waiting_for_response = false;
579 self.waiting_started = None;
580 self.executing_tools.clear();
581 self.conversation_view.complete_streaming();
583 self.current_turn_id = None;
585 self.conversation_view.add_system_message("Request cancelled".to_string());
586 }
587 }
588 }
589
590 fn execute_command(&mut self, input: &str) {
592 let Some((cmd_name, args)) = parse_command(input) else {
593 self.conversation_view.add_system_message("Invalid command format".to_string());
594 return;
595 };
596
597 let cmd_idx = self.commands.iter().position(|c| c.name() == cmd_name);
599 let Some(cmd_idx) = cmd_idx else {
600 self.conversation_view.add_system_message(format!("Unknown command: /{}", cmd_name));
601 return;
602 };
603
604 let (result, pending_actions) = {
606 let extension = self.command_extension.take();
608 let extension_ref = extension.as_ref().map(|e| e.as_ref() as &dyn std::any::Any);
609
610 let mut ctx = CommandContext::new(
611 self.session_id,
612 &self.agent_name,
613 &self.version,
614 &self.commands,
615 &mut *self.conversation_view,
616 self.to_controller.as_ref(),
617 extension_ref,
618 );
619
620 let result = self.commands[cmd_idx].execute(args, &mut ctx);
622 let pending_actions = ctx.take_pending_actions();
623
624 self.command_extension = extension;
626
627 (result, pending_actions)
628 };
629
630 for action in pending_actions {
632 match action {
633 PendingAction::OpenThemePicker => self.cmd_themes(),
634 PendingAction::OpenSessionPicker => self.cmd_sessions(),
635 PendingAction::ClearConversation => self.cmd_clear(),
636 PendingAction::CompactConversation => self.cmd_compact(),
637 PendingAction::CreateNewSession => { self.cmd_new_session(); }
638 PendingAction::Quit => { self.should_quit = true; }
639 }
640 }
641
642 match result {
644 CommandResult::Ok | CommandResult::Handled => {}
645 CommandResult::Message(msg) => {
646 self.conversation_view.add_system_message(msg);
647 }
648 CommandResult::Error(err) => {
649 self.conversation_view.add_system_message(format!("Error: {}", err));
650 }
651 CommandResult::Quit => {
652 self.should_quit = true;
653 }
654 }
655 }
656
657 fn cmd_clear(&mut self) {
658 self.conversation_view = (self.conversation_factory)();
660 self.user_turn_counter = 0;
661
662 if self.session_id != 0 {
664 if let Some(ref tx) = self.to_controller {
665 let payload =
666 ControllerInputPayload::control(self.session_id, ControlCmd::Clear);
667 if let Err(e) = tx.try_send(payload) {
668 tracing::warn!("Failed to send clear command to controller: {}", e);
669 }
670 }
671 }
672 }
673
674 fn cmd_compact(&mut self) {
675 if self.session_id == 0 {
677 self.conversation_view.add_system_message("No active session to compact".to_string());
678 return;
679 }
680
681 if let Some(ref tx) = self.to_controller {
683 let payload = ControllerInputPayload::control(self.session_id, ControlCmd::Compact);
684 if tx.try_send(payload).is_ok() {
685 self.waiting_for_response = true;
687 self.waiting_started = Some(Instant::now());
688 self.custom_throbber_message = Some("compacting...".to_string());
689 } else {
690 self.conversation_view.add_system_message("Failed to send compact command".to_string());
691 }
692 }
693 }
694
695 fn cmd_new_session(&mut self) -> String {
696 let Some(ref controller) = self.controller else {
697 return "Error: Controller not available".to_string();
698 };
699
700 let Some(ref handle) = self.runtime_handle else {
701 return "Error: Runtime not available".to_string();
702 };
703
704 let Some(ref registry) = self.llm_registry else {
705 return "Error: No LLM providers configured.\nSet ANTHROPIC_API_KEY or create config file".to_string();
706 };
707
708 let Some(config) = registry.get_default() else {
710 return "Error: No LLM providers configured.\nSet ANTHROPIC_API_KEY or create config file".to_string();
711 };
712
713 let model = config.model.clone();
714 let context_limit = config.context_limit;
715 let config = config.clone();
716
717 let controller = controller.clone();
719 let session_id = match handle.block_on(async { controller.create_session(config).await }) {
720 Ok(id) => id,
721 Err(e) => {
722 return format!("Error: Failed to create session: {}", e);
723 }
724 };
725
726 let session_info = SessionInfo::new(session_id, model.clone(), context_limit);
728 self.sessions.push(session_info);
729
730 if self.session_id != 0 {
732 let state = self.conversation_view.save_state();
733 self.session_states.insert(self.session_id, state);
734 }
735
736 self.conversation_view = (self.conversation_factory)();
738
739 self.session_id = session_id;
740 self.model_name = model.clone();
741 self.context_limit = context_limit;
742 self.context_used = 0;
743 self.user_turn_counter = 0;
744
745 String::new()
747 }
748
749 fn cmd_themes(&mut self) {
750 if let Some(widget) = self.widgets.get_mut(widget_ids::THEME_PICKER) {
751 if let Some(picker) = widget.as_any_mut().downcast_mut::<ThemePickerState>() {
752 let current_name = current_theme_name();
753 let current_theme = app_theme();
754 picker.activate(¤t_name, current_theme);
755 }
756 }
757 }
758
759 fn cmd_sessions(&mut self) {
760 if let Some(session) = self.sessions.iter_mut().find(|s| s.id == self.session_id) {
762 session.context_used = self.context_used;
763 }
764
765 if let Some(widget) = self.widgets.get_mut(widget_ids::SESSION_PICKER) {
766 if let Some(picker) = widget.as_any_mut().downcast_mut::<SessionPickerState>() {
767 picker.activate(self.sessions.clone(), self.session_id);
768 }
769 }
770 }
771
772 pub fn switch_session(&mut self, session_id: i64) {
774 if session_id == self.session_id {
776 return;
777 }
778
779 if let Some(session) = self.sessions.iter_mut().find(|s| s.id == self.session_id) {
781 session.context_used = self.context_used;
782 }
783
784 let state = self.conversation_view.save_state();
786 self.session_states.insert(self.session_id, state);
787
788 if let Some(session) = self.sessions.iter().find(|s| s.id == session_id) {
790 self.session_id = session_id;
791 self.model_name = session.model.clone();
792 self.context_used = session.context_used;
793 self.context_limit = session.context_limit;
794 self.user_turn_counter = 0;
795
796 if let Some(stored_state) = self.session_states.remove(&session_id) {
798 self.conversation_view.restore_state(stored_state);
799 } else {
800 self.conversation_view = (self.conversation_factory)();
801 }
802 }
803 }
804
805 pub fn add_session(&mut self, info: SessionInfo) {
807 self.sessions.push(info);
808 }
809
810 fn submit_question_panel_response(&mut self, tool_use_id: String, response: crate::controller::AskUserQuestionsResponse) {
812 if let (Some(registry), Some(handle)) =
814 (&self.user_interaction_registry, &self.runtime_handle)
815 {
816 let registry = registry.clone();
817 handle.spawn(async move {
818 if let Err(e) = registry.respond(&tool_use_id, response).await {
819 tracing::error!(%tool_use_id, ?e, "Failed to respond to interaction");
820 }
821 });
822 }
823
824 if let Some(widget) = self.widgets.get_mut(widget_ids::QUESTION_PANEL) {
826 if let Some(panel) = widget.as_any_mut().downcast_mut::<QuestionPanel>() {
827 panel.deactivate();
828 }
829 }
830 }
831
832 fn cancel_question_panel_response(&mut self, tool_use_id: String) {
834 if let (Some(registry), Some(handle)) =
836 (&self.user_interaction_registry, &self.runtime_handle)
837 {
838 let registry = registry.clone();
839 handle.spawn(async move {
840 if let Err(e) = registry.cancel(&tool_use_id).await {
841 tracing::warn!(%tool_use_id, ?e, "Failed to cancel interaction");
842 }
843 });
844 }
845
846 if let Some(widget) = self.widgets.get_mut(widget_ids::QUESTION_PANEL) {
848 if let Some(panel) = widget.as_any_mut().downcast_mut::<QuestionPanel>() {
849 panel.deactivate();
850 }
851 }
852 }
853
854 fn submit_permission_panel_response(&mut self, tool_use_id: String, response: PermissionResponse) {
856 if let (Some(registry), Some(handle)) = (&self.permission_registry, &self.runtime_handle) {
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 permission request");
862 }
863 });
864 }
865
866 if let Some(widget) = self.widgets.get_mut(widget_ids::PERMISSION_PANEL) {
868 if let Some(panel) = widget.as_any_mut().downcast_mut::<PermissionPanel>() {
869 panel.deactivate();
870 }
871 }
872 }
873
874 fn cancel_permission_panel_response(&mut self, tool_use_id: String) {
876 if let (Some(registry), Some(handle)) = (&self.permission_registry, &self.runtime_handle) {
878 let registry = registry.clone();
879 handle.spawn(async move {
880 if let Err(e) = registry.cancel(&tool_use_id).await {
881 tracing::warn!(%tool_use_id, ?e, "Failed to cancel permission request");
882 }
883 });
884 }
885
886 if let Some(widget) = self.widgets.get_mut(widget_ids::PERMISSION_PANEL) {
888 if let Some(panel) = widget.as_any_mut().downcast_mut::<PermissionPanel>() {
889 panel.deactivate();
890 }
891 }
892 }
893
894 fn process_controller_messages(&mut self) {
896 let mut messages = Vec::new();
898
899 if let Some(ref mut rx) = self.from_controller {
900 loop {
901 match rx.try_recv() {
902 Ok(msg) => messages.push(msg),
903 Err(mpsc::error::TryRecvError::Empty) => break,
904 Err(mpsc::error::TryRecvError::Disconnected) => {
905 tracing::warn!("Controller channel disconnected");
906 break;
907 }
908 }
909 }
910 }
911
912 for msg in messages {
914 self.handle_ui_message(msg);
915 }
916 }
917
918 fn handle_ui_message(&mut self, msg: UiMessage) {
920 match msg {
921 UiMessage::TextChunk { text, turn_id, .. } => {
922 if !self.is_current_turn(&turn_id) {
924 return;
925 }
926 self.conversation_view.append_streaming(&text);
927 }
928 UiMessage::Display { message, .. } => {
929 self.conversation_view.add_system_message(message);
930 }
931 UiMessage::Complete {
932 turn_id,
933 stop_reason,
934 ..
935 } => {
936 if !self.is_current_turn(&turn_id) {
938 return;
939 }
940
941 let is_tool_use = stop_reason.as_deref() == Some("tool_use");
943
944 self.conversation_view.complete_streaming();
945
946 if !is_tool_use {
948 self.waiting_for_response = false;
949 self.waiting_started = None;
950 }
951 }
952 UiMessage::TokenUpdate {
953 input_tokens,
954 context_limit,
955 ..
956 } => {
957 self.context_used = input_tokens;
958 self.context_limit = context_limit;
959 }
960 UiMessage::Error { error, turn_id, .. } => {
961 if !self.is_current_turn(&turn_id) {
962 return;
963 }
964 self.conversation_view.complete_streaming();
965 self.waiting_for_response = false;
966 self.waiting_started = None;
967 self.current_turn_id = None;
968 self.conversation_view.add_system_message(format!("Error: {}", error));
969 }
970 UiMessage::System { message, .. } => {
971 self.conversation_view.add_system_message(message);
972 }
973 UiMessage::ToolExecuting {
974 tool_use_id,
975 display_name,
976 display_title,
977 ..
978 } => {
979 self.executing_tools.insert(tool_use_id.clone());
980 self.conversation_view.add_tool_message(&tool_use_id, &display_name, &display_title);
981 }
982 UiMessage::ToolCompleted {
983 tool_use_id,
984 status,
985 error,
986 ..
987 } => {
988 self.executing_tools.remove(&tool_use_id);
989 let tool_status = if status == ToolResultStatus::Success {
990 ToolStatus::Completed
991 } else {
992 ToolStatus::Failed(error.unwrap_or_default())
993 };
994 self.conversation_view.update_tool_status(&tool_use_id, tool_status);
995 }
996 UiMessage::CommandComplete {
997 command,
998 success,
999 message,
1000 ..
1001 } => {
1002 self.waiting_for_response = false;
1003 self.waiting_started = None;
1004 self.custom_throbber_message = None;
1005
1006 match command {
1007 ControlCmd::Compact => {
1008 if let Some(msg) = message {
1009 self.conversation_view.add_system_message(msg);
1010 }
1011 }
1012 ControlCmd::Clear => {}
1013 _ => {
1014 tracing::debug!(?command, ?success, "Command completed");
1015 }
1016 }
1017 }
1018 UiMessage::UserInteractionRequired {
1019 session_id,
1020 tool_use_id,
1021 request,
1022 turn_id,
1023 } => {
1024 if session_id == self.session_id {
1025 self.conversation_view.update_tool_status(&tool_use_id, ToolStatus::WaitingForUser);
1026 if let Some(widget) = self.widgets.get_mut(widget_ids::QUESTION_PANEL) {
1028 if let Some(panel) = widget.as_any_mut().downcast_mut::<QuestionPanel>() {
1029 panel.activate(tool_use_id, session_id, request, turn_id);
1030 }
1031 }
1032 }
1033 }
1034 UiMessage::PermissionRequired {
1035 session_id,
1036 tool_use_id,
1037 request,
1038 turn_id,
1039 } => {
1040 if session_id == self.session_id {
1041 self.conversation_view.update_tool_status(&tool_use_id, ToolStatus::WaitingForUser);
1042 if let Some(widget) = self.widgets.get_mut(widget_ids::PERMISSION_PANEL) {
1044 if let Some(panel) = widget.as_any_mut().downcast_mut::<PermissionPanel>() {
1045 panel.activate(tool_use_id, session_id, request, turn_id);
1046 }
1047 }
1048 }
1049 }
1050 }
1051 }
1052
1053 fn is_current_turn(&self, turn_id: &Option<TurnId>) -> bool {
1055 match (&self.current_turn_id, turn_id) {
1056 (Some(current), Some(incoming)) => current == incoming,
1057 (None, _) => false,
1058 (Some(_), None) => false,
1059 }
1060 }
1061
1062 pub fn scroll_up(&mut self) {
1063 self.conversation_view.scroll_up();
1064 }
1065
1066 pub fn scroll_down(&mut self) {
1067 self.conversation_view.scroll_down();
1068 }
1069
1070 fn get_cwd(&self) -> String {
1072 std::env::current_dir()
1073 .map(|p| {
1074 let path_str = p.display().to_string();
1075 if let Some(home) = std::env::var_os("HOME") {
1076 let home_str = home.to_string_lossy();
1077 if path_str.starts_with(home_str.as_ref()) {
1078 return format!("~{}", &path_str[home_str.len()..]);
1079 }
1080 }
1081 path_str
1082 })
1083 .unwrap_or_else(|_| "unknown".to_string())
1084 }
1085
1086 fn handle_key(&mut self, key: KeyCode, modifiers: KeyModifiers) {
1088 let key_event = KeyEvent::new(key, modifiers);
1090 let context = KeyContext {
1091 input_empty: self.input().map(|i| i.is_empty()).unwrap_or(true),
1092 is_processing: self.waiting_for_response || self.is_chat_streaming(),
1093 widget_blocking: self.any_widget_blocks_input(),
1094 };
1095
1096 let result = self.key_handler.handle_key(key_event, &context);
1099
1100 match result {
1101 AppKeyResult::Handled => return,
1102 AppKeyResult::Action(action) => {
1103 self.execute_key_action(action);
1104 return;
1105 }
1106 AppKeyResult::NotHandled => {
1107 }
1109 }
1110
1111 let theme = app_theme();
1113 let nav = NavigationHelper::new(self.key_handler.bindings());
1114 let widget_ctx = WidgetKeyContext { theme: &theme, nav };
1115
1116 let widget_ids_to_check: Vec<&'static str> = self.widget_priority_order.clone();
1118
1119 for widget_id in widget_ids_to_check {
1120 if let Some(widget) = self.widgets.get_mut(widget_id) {
1121 if widget.is_active() {
1122 match widget.handle_key(key_event, &widget_ctx) {
1123 WidgetKeyResult::Handled => return,
1124 WidgetKeyResult::Action(action) => {
1125 self.process_widget_action(action);
1126 return;
1127 }
1128 WidgetKeyResult::NotHandled => {
1129 }
1131 }
1132 }
1133 }
1134 }
1135
1136 if self.is_slash_popup_active() {
1138 self.handle_slash_popup_key(key);
1139 return;
1140 }
1141 }
1142
1143 fn execute_key_action(&mut self, action: AppKeyAction) {
1145 match action {
1146 AppKeyAction::MoveUp => {
1147 if let Some(input) = self.input_mut() {
1148 input.move_up();
1149 }
1150 }
1151 AppKeyAction::MoveDown => {
1152 if let Some(input) = self.input_mut() {
1153 input.move_down();
1154 }
1155 }
1156 AppKeyAction::MoveLeft => {
1157 if let Some(input) = self.input_mut() {
1158 input.move_left();
1159 }
1160 }
1161 AppKeyAction::MoveRight => {
1162 if let Some(input) = self.input_mut() {
1163 input.move_right();
1164 }
1165 }
1166 AppKeyAction::MoveLineStart => {
1167 if let Some(input) = self.input_mut() {
1168 input.move_to_line_start();
1169 }
1170 }
1171 AppKeyAction::MoveLineEnd => {
1172 if let Some(input) = self.input_mut() {
1173 input.move_to_line_end();
1174 }
1175 }
1176 AppKeyAction::DeleteCharBefore => {
1177 if let Some(input) = self.input_mut() {
1178 input.delete_char_before();
1179 }
1180 }
1181 AppKeyAction::DeleteCharAt => {
1182 if let Some(input) = self.input_mut() {
1183 input.delete_char_at();
1184 }
1185 }
1186 AppKeyAction::KillLine => {
1187 if let Some(input) = self.input_mut() {
1188 input.kill_line();
1189 }
1190 }
1191 AppKeyAction::InsertNewline => {
1192 if let Some(input) = self.input_mut() {
1193 input.insert_char('\n');
1194 }
1195 }
1196 AppKeyAction::InsertChar(c) => {
1197 if let Some(input) = self.input_mut() {
1198 input.insert_char(c);
1199 }
1200 if c == '/' && self.input().map(|i| i.buffer() == "/").unwrap_or(false) {
1202 self.activate_slash_popup();
1203 }
1204 }
1205 AppKeyAction::Submit => {
1206 self.submit_message();
1207 }
1208 AppKeyAction::Interrupt => {
1209 self.interrupt_request();
1210 }
1211 AppKeyAction::Quit => {
1212 self.should_quit = true;
1213 }
1214 AppKeyAction::RequestExit => {
1215 let should_quit = self.exit_handler
1217 .as_mut()
1218 .map(|h| h.on_exit())
1219 .unwrap_or(true);
1220 if should_quit {
1221 self.should_quit = true;
1222 }
1223 }
1224 AppKeyAction::ActivateSlashPopup => {
1225 self.activate_slash_popup();
1226 }
1227 AppKeyAction::Custom(_) => {
1228 }
1232 }
1233 }
1234
1235 fn process_widget_action(&mut self, action: WidgetAction) {
1237 match action {
1238 WidgetAction::SubmitQuestion { tool_use_id, response } => {
1239 self.submit_question_panel_response(tool_use_id, response);
1240 }
1241 WidgetAction::CancelQuestion { tool_use_id } => {
1242 self.cancel_question_panel_response(tool_use_id);
1243 }
1244 WidgetAction::SubmitPermission { tool_use_id, response } => {
1245 self.submit_permission_panel_response(tool_use_id, response);
1246 }
1247 WidgetAction::CancelPermission { tool_use_id } => {
1248 self.cancel_permission_panel_response(tool_use_id);
1249 }
1250 WidgetAction::SwitchSession { session_id } => {
1251 self.switch_session(session_id);
1252 }
1253 WidgetAction::ExecuteCommand { command } => {
1254 if command.starts_with("__SLASH_INDEX_") {
1256 if let Ok(idx) = command.trim_start_matches("__SLASH_INDEX_").parse::<usize>() {
1257 self.execute_slash_command_at_index(idx);
1258 }
1259 } else {
1260 self.execute_command(&command);
1261 }
1262 }
1263 WidgetAction::Close => {
1264 }
1266 }
1267 }
1268
1269 fn is_slash_popup_active(&self) -> bool {
1271 self.widgets
1272 .get(widget_ids::SLASH_POPUP)
1273 .map(|w| w.is_active())
1274 .unwrap_or(false)
1275 }
1276
1277 fn filter_command_indices(&self, prefix: &str) -> Vec<usize> {
1279 let search_term = prefix.trim_start_matches('/').to_lowercase();
1280 self.commands
1281 .iter()
1282 .enumerate()
1283 .filter(|(_, cmd)| cmd.name().to_lowercase().starts_with(&search_term))
1284 .map(|(i, _)| i)
1285 .collect()
1286 }
1287
1288 fn activate_slash_popup(&mut self) {
1290 let indices = self.filter_command_indices("/");
1292 let count = indices.len();
1293 self.filtered_command_indices = indices;
1294
1295 if let Some(widget) = self.widgets.get_mut(widget_ids::SLASH_POPUP) {
1296 if let Some(popup) = widget.as_any_mut().downcast_mut::<SlashPopupState>() {
1297 popup.activate();
1298 popup.set_filtered_count(count);
1299 }
1300 }
1301 }
1302
1303 fn handle_slash_popup_key(&mut self, key: KeyCode) {
1305 match key {
1306 KeyCode::Up => {
1307 if let Some(widget) = self.widgets.get_mut(widget_ids::SLASH_POPUP) {
1308 if let Some(popup) = widget.as_any_mut().downcast_mut::<SlashPopupState>() {
1309 popup.select_previous();
1310 }
1311 }
1312 }
1313 KeyCode::Down => {
1314 if let Some(widget) = self.widgets.get_mut(widget_ids::SLASH_POPUP) {
1315 if let Some(popup) = widget.as_any_mut().downcast_mut::<SlashPopupState>() {
1316 popup.select_next();
1317 }
1318 }
1319 }
1320 KeyCode::Enter => {
1321 let selected_idx = self.widgets
1322 .get(widget_ids::SLASH_POPUP)
1323 .and_then(|w| w.as_any().downcast_ref::<SlashPopupState>())
1324 .map(|p| p.selected_index)
1325 .unwrap_or(0);
1326 self.execute_slash_command_at_index(selected_idx);
1327 }
1328 KeyCode::Esc => {
1329 if let Some(input) = self.input_mut() {
1330 input.clear();
1331 }
1332 if let Some(widget) = self.widgets.get_mut(widget_ids::SLASH_POPUP) {
1333 if let Some(popup) = widget.as_any_mut().downcast_mut::<SlashPopupState>() {
1334 popup.deactivate();
1335 }
1336 }
1337 self.filtered_command_indices.clear();
1338 }
1339 KeyCode::Backspace => {
1340 let is_just_slash = self.input().map(|i| i.buffer() == "/").unwrap_or(false);
1341 if is_just_slash {
1342 if let Some(input) = self.input_mut() {
1343 input.clear();
1344 }
1345 if let Some(widget) = self.widgets.get_mut(widget_ids::SLASH_POPUP) {
1346 if let Some(popup) = widget.as_any_mut().downcast_mut::<SlashPopupState>() {
1347 popup.deactivate();
1348 }
1349 }
1350 self.filtered_command_indices.clear();
1351 } else {
1352 if let Some(input) = self.input_mut() {
1353 input.delete_char_before();
1354 }
1355 let buffer = self.input().map(|i| i.buffer().to_string()).unwrap_or_default();
1356 self.filtered_command_indices = self.filter_command_indices(&buffer);
1357 if let Some(widget) = self.widgets.get_mut(widget_ids::SLASH_POPUP) {
1358 if let Some(popup) = widget.as_any_mut().downcast_mut::<SlashPopupState>() {
1359 popup.set_filtered_count(self.filtered_command_indices.len());
1360 }
1361 }
1362 }
1363 }
1364 KeyCode::Char(c) => {
1365 if let Some(input) = self.input_mut() {
1366 input.insert_char(c);
1367 }
1368 let buffer = self.input().map(|i| i.buffer().to_string()).unwrap_or_default();
1369 self.filtered_command_indices = self.filter_command_indices(&buffer);
1370 if let Some(widget) = self.widgets.get_mut(widget_ids::SLASH_POPUP) {
1371 if let Some(popup) = widget.as_any_mut().downcast_mut::<SlashPopupState>() {
1372 popup.set_filtered_count(self.filtered_command_indices.len());
1373 }
1374 }
1375 }
1376 _ => {
1377 if let Some(widget) = self.widgets.get_mut(widget_ids::SLASH_POPUP) {
1378 if let Some(popup) = widget.as_any_mut().downcast_mut::<SlashPopupState>() {
1379 popup.deactivate();
1380 }
1381 }
1382 }
1383 }
1384 }
1385
1386 fn execute_slash_command_at_index(&mut self, idx: usize) {
1388 if let Some(&cmd_idx) = self.filtered_command_indices.get(idx) {
1390 if let Some(cmd) = self.commands.get(cmd_idx) {
1391 let cmd_name = cmd.name().to_string();
1392 if let Some(input) = self.input_mut() {
1393 input.clear();
1394 for c in format!("/{}", cmd_name).chars() {
1395 input.insert_char(c);
1396 }
1397 }
1398 if let Some(widget) = self.widgets.get_mut(widget_ids::SLASH_POPUP) {
1399 if let Some(popup) = widget.as_any_mut().downcast_mut::<SlashPopupState>() {
1400 popup.deactivate();
1401 }
1402 }
1403 self.filtered_command_indices.clear();
1404 self.submit_message();
1405 }
1406 }
1407 }
1408
1409 pub fn run(&mut self) -> io::Result<()> {
1410 enable_raw_mode()?;
1411 io::stdout().execute(EnterAlternateScreen)?;
1412 io::stdout().execute(EnableMouseCapture)?;
1413
1414 let mut terminal = Terminal::new(CrosstermBackend::new(io::stdout()))?;
1415
1416 while !self.should_quit {
1417 self.process_controller_messages();
1418
1419 let show_throbber = self.waiting_for_response
1420 || self.is_chat_streaming()
1421 || !self.executing_tools.is_empty();
1422
1423 if show_throbber {
1425 self.animation_frame_counter = self.animation_frame_counter.wrapping_add(1);
1426 if self.animation_frame_counter % 6 == 0 {
1427 self.throbber_state.calc_next();
1428 self.conversation_view.step_spinner();
1429 }
1430 }
1431
1432 let prompt_len = PROMPT.chars().count();
1433 let indent_len = CONTINUATION_INDENT.len();
1434
1435 terminal.draw(|frame| {
1436 self.render_frame(frame, show_throbber, prompt_len, indent_len);
1437 })?;
1438
1439 let mut net_scroll: i32 = 0;
1441
1442 while event::poll(std::time::Duration::from_millis(0))? {
1443 match event::read()? {
1444 Event::Key(key) => {
1445 if key.kind == KeyEventKind::Press {
1446 self.handle_key(key.code, key.modifiers);
1447 }
1448 }
1449 Event::Mouse(mouse) => match mouse.kind {
1450 MouseEventKind::ScrollUp => net_scroll -= 1,
1451 MouseEventKind::ScrollDown => net_scroll += 1,
1452 _ => {}
1453 },
1454 _ => {}
1455 }
1456 }
1457
1458 if net_scroll < 0 {
1460 for _ in 0..(-net_scroll) {
1461 self.scroll_up();
1462 }
1463 } else if net_scroll > 0 {
1464 for _ in 0..net_scroll {
1465 self.scroll_down();
1466 }
1467 }
1468
1469 if net_scroll == 0 {
1470 std::thread::sleep(std::time::Duration::from_millis(16));
1471 }
1472 }
1473
1474 io::stdout().execute(DisableMouseCapture)?;
1475 disable_raw_mode()?;
1476 io::stdout().execute(LeaveAlternateScreen)?;
1477
1478 Ok(())
1479 }
1480
1481 fn render_frame(
1482 &mut self,
1483 frame: &mut ratatui::Frame,
1484 show_throbber: bool,
1485 prompt_len: usize,
1486 indent_len: usize,
1487 ) {
1488 let frame_area = frame.area();
1489 let frame_width = frame_area.width as usize;
1490 let frame_height = frame_area.height;
1491 let theme = app_theme();
1492
1493 let ctx = self.build_layout_context(frame_area, show_throbber, prompt_len, indent_len, &theme);
1495 let sizes = self.compute_widget_sizes(frame_height);
1496 let layout = self.layout_template.compute(&ctx, &sizes);
1497
1498 let theme_picker_active = sizes.is_active(widget_ids::THEME_PICKER);
1500 let session_picker_active = sizes.is_active(widget_ids::SESSION_PICKER);
1501 let question_panel_active = sizes.is_active(widget_ids::QUESTION_PANEL);
1502 let permission_panel_active = sizes.is_active(widget_ids::PERMISSION_PANEL);
1503
1504 let status_bar_data = StatusBarData {
1506 cwd: self.get_cwd(),
1507 model_name: self.model_name.clone(),
1508 context_used: self.context_used,
1509 context_limit: self.context_limit,
1510 session_id: self.session_id,
1511 status_hint: self.key_handler.status_hint(),
1512 is_waiting: show_throbber,
1513 waiting_elapsed: self.waiting_started.map(|t| t.elapsed()),
1514 input_empty: self.input().map(|i| i.is_empty()).unwrap_or(true),
1515 panels_active: question_panel_active || permission_panel_active,
1516 };
1517
1518 if let Some(widget) = self.widgets.get_mut(widget_ids::STATUS_BAR) {
1520 if let Some(status_bar) = widget.as_any_mut().downcast_mut::<StatusBar>() {
1521 status_bar.update_data(status_bar_data);
1522 }
1523 }
1524
1525 for widget_id in &layout.render_order {
1527 if *widget_id == widget_ids::THEME_PICKER || *widget_id == widget_ids::SESSION_PICKER {
1529 continue;
1530 }
1531
1532 let Some(area) = layout.widget_areas.get(widget_id) else {
1534 continue;
1535 };
1536
1537 match *widget_id {
1539 id if id == widget_ids::CHAT_VIEW => {
1540 let pending_status: Option<&str> = if !self.executing_tools.is_empty() {
1542 Some(PENDING_STATUS_TOOLS)
1543 } else if self.waiting_for_response && !self.is_chat_streaming() {
1544 Some(PENDING_STATUS_LLM)
1545 } else {
1546 None
1547 };
1548 self.conversation_view.render(frame, *area, &theme, pending_status);
1549 }
1550 id if id == widget_ids::TEXT_INPUT => {
1551 }
1553 id if id == widget_ids::SLASH_POPUP => {
1554 if let Some(widget) = self.widgets.get(widget_ids::SLASH_POPUP) {
1555 if let Some(popup_state) = widget.as_any().downcast_ref::<SlashPopupState>() {
1556 let filtered: Vec<&dyn SlashCommand> = self.filtered_command_indices
1558 .iter()
1559 .filter_map(|&i| self.commands.get(i).map(|c| c.as_ref()))
1560 .collect();
1561 render_slash_popup(
1562 popup_state,
1563 &filtered,
1564 frame,
1565 *area,
1566 &theme,
1567 );
1568 }
1569 }
1570 }
1571 _ => {
1572 if let Some(widget) = self.widgets.get_mut(widget_id) {
1574 if widget.is_active() {
1575 widget.render(frame, *area, &theme);
1576 }
1577 }
1578 }
1579 }
1580 }
1581
1582 if let Some(input_area) = layout.input_area {
1584 if !question_panel_active && !permission_panel_active {
1585 if show_throbber {
1586 let default_message;
1587 let message = if let Some(msg) = &self.custom_throbber_message {
1588 msg.as_str()
1589 } else if let Some(ref msg_fn) = self.processing_message_fn {
1590 default_message = msg_fn();
1591 &default_message
1592 } else {
1593 &self.processing_message
1594 };
1595 let throbber = Throbber::default()
1596 .label(message)
1597 .style(theme.throbber_label)
1598 .throbber_style(theme.throbber_spinner)
1599 .throbber_set(BRAILLE_EIGHT_DOUBLE);
1600
1601 let throbber_block = Block::default()
1602 .borders(Borders::TOP | Borders::BOTTOM)
1603 .border_style(theme.input_border);
1604 let inner = throbber_block.inner(input_area);
1605 let throbber_inner = Rect::new(
1606 inner.x + 1,
1607 inner.y,
1608 inner.width.saturating_sub(1),
1609 inner.height,
1610 );
1611 frame.render_widget(throbber_block, input_area);
1612 frame.render_stateful_widget(throbber, throbber_inner, &mut self.throbber_state);
1613 } else if let Some(input) = self.input() {
1614 let input_lines: Vec<String> = input
1615 .buffer()
1616 .split('\n')
1617 .enumerate()
1618 .map(|(i, line)| {
1619 if i == 0 {
1620 format!("{}{}", PROMPT, line)
1621 } else {
1622 format!("{}{}", CONTINUATION_INDENT, line)
1623 }
1624 })
1625 .collect();
1626 let input_text = if input_lines.is_empty() {
1627 PROMPT.to_string()
1628 } else {
1629 input_lines.join("\n")
1630 };
1631
1632 let input_box = Paragraph::new(input_text)
1633 .block(
1634 Block::default()
1635 .borders(Borders::TOP | Borders::BOTTOM)
1636 .border_style(theme.input_border),
1637 )
1638 .wrap(Wrap { trim: false });
1639 frame.render_widget(input_box, input_area);
1640
1641 if !theme_picker_active && !session_picker_active {
1643 let (cursor_rel_x, cursor_rel_y) = input
1644 .cursor_display_position_wrapped(frame_width, prompt_len, indent_len);
1645 let cursor_x = input_area.x + cursor_rel_x;
1646 let cursor_y = input_area.y + 1 + cursor_rel_y;
1647 frame.set_cursor_position((cursor_x, cursor_y));
1648 }
1649 }
1650 }
1651 }
1652
1653
1654 if theme_picker_active {
1656 if let Some(widget) = self.widgets.get(widget_ids::THEME_PICKER) {
1657 if let Some(picker) = widget.as_any().downcast_ref::<ThemePickerState>() {
1658 render_theme_picker(picker, frame, frame_area);
1659 }
1660 }
1661 }
1662
1663 if session_picker_active {
1664 if let Some(widget) = self.widgets.get(widget_ids::SESSION_PICKER) {
1665 if let Some(picker) = widget.as_any().downcast_ref::<SessionPickerState>() {
1666 render_session_picker(picker, frame, frame_area, &theme);
1667 }
1668 }
1669 }
1670 }
1671}
1672
1673impl Default for App {
1674 fn default() -> Self {
1675 Self::new()
1676 }
1677}