use warpui::{Entity, ModelContext, ModelHandle}; use crate::ai::agent::conversation::AIConversationId; use crate::terminal::input::buffer_model::InputBufferModel; use crate::terminal::input::inline_menu::InlineMenuType; use super::{BufferState, DynamicEnumSuggestionStatus, InputConfig, InputSuggestionsMode}; /// Model responsible for managing the input suggestions mode state. pub struct InputSuggestionsModeModel { mode: InputSuggestionsMode, /// Buffer state saved when an inline menu is opened, so it can be restored on dismiss. buffer_to_restore: Option, /// Handle to the input buffer model, used to snapshot buffer state when opening menus. buffer_model: ModelHandle, } impl InputSuggestionsModeModel { pub fn new(buffer_model: ModelHandle) -> Self { Self { mode: InputSuggestionsMode::Closed, buffer_to_restore: None, buffer_model, } } pub fn mode(&self) -> &InputSuggestionsMode { &self.mode } pub fn set_mode(&mut self, mode: InputSuggestionsMode, ctx: &mut ModelContext) { if self.mode == mode { return; } let input_config_to_restore = self.mode.input_config_to_restore(); // If we're setting a new non-closed mode while the current mode is also non-closed, // first emit a mode change for the implicit close before transitioning to the new mode. if self.is_visible() && !matches!(mode, InputSuggestionsMode::Closed) { self.mode = InputSuggestionsMode::Closed; ctx.emit(InputSuggestionsModeEvent::ModeChanged { buffer_to_restore: None, input_config_to_restore, }); } // Snapshot the buffer state when opening a mode that supports buffer restoration. if mode.should_snapshot_and_restore_buffer() { let buffer_model = self.buffer_model.as_ref(ctx); self.buffer_to_restore = Some(BufferState::new( buffer_model.current_value().to_owned(), buffer_model.cursor_point(), )); } // When closing via set_mode, we always discard saved buffer state. // To restore buffer state, callers should use close_and_restore_buffer. if matches!(mode, InputSuggestionsMode::Closed) { self.buffer_to_restore = None; } self.mode = mode; ctx.emit(InputSuggestionsModeEvent::ModeChanged { buffer_to_restore: None, input_config_to_restore: None, }); } /// Closes the current menu, restoring the buffer if it was snapshotted on open. pub fn close_and_restore_buffer(&mut self, ctx: &mut ModelContext) { if self.is_closed() { return; } let buffer_to_restore = self.buffer_to_restore.take(); let input_config_to_restore = self.mode.input_config_to_restore(); self.mode = InputSuggestionsMode::Closed; ctx.emit(InputSuggestionsModeEvent::ModeChanged { buffer_to_restore, input_config_to_restore, }); } pub fn set_dynamic_enum_status( &mut self, status: DynamicEnumSuggestionStatus, ctx: &mut ModelContext, ) { if let InputSuggestionsMode::DynamicWorkflowEnumSuggestions { dynamic_enum_status, .. } = &mut self.mode { *dynamic_enum_status = status; ctx.emit(InputSuggestionsModeEvent::ModeChanged { buffer_to_restore: None, input_config_to_restore: None, }); } } pub fn is_visible(&self) -> bool { self.mode.is_visible() } pub fn is_closed(&self) -> bool { matches!(self.mode, InputSuggestionsMode::Closed) } pub fn is_history_up(&self) -> bool { matches!(self.mode, InputSuggestionsMode::HistoryUp { .. }) } pub fn is_completion_suggestions(&self) -> bool { matches!( self.mode, InputSuggestionsMode::CompletionSuggestions { .. } ) } pub fn is_static_workflow_enum_suggestions(&self) -> bool { matches!( self.mode, InputSuggestionsMode::StaticWorkflowEnumSuggestions { .. } ) } pub fn is_dynamic_workflow_enum_suggestions(&self) -> bool { matches!( self.mode, InputSuggestionsMode::DynamicWorkflowEnumSuggestions { .. } ) } pub fn is_ai_context_menu(&self) -> bool { matches!(self.mode, InputSuggestionsMode::AIContextMenu { .. }) } pub fn is_slash_commands(&self) -> bool { matches!(self.mode, InputSuggestionsMode::SlashCommands) } pub fn is_conversation_menu(&self) -> bool { matches!(self.mode, InputSuggestionsMode::ConversationMenu) } pub fn is_inline_model_selector(&self) -> bool { matches!(self.mode, InputSuggestionsMode::ModelSelector) } pub fn is_profile_selector(&self) -> bool { matches!(self.mode, InputSuggestionsMode::ProfileSelector) } pub fn is_prompts_menu(&self) -> bool { matches!(self.mode, InputSuggestionsMode::PromptsMenu) } pub fn is_skill_menu(&self) -> bool { matches!(self.mode, InputSuggestionsMode::SkillMenu) } pub fn is_user_query_menu(&self) -> bool { matches!( self.mode, InputSuggestionsMode::UserQueryMenu { action: super::UserQueryMenuAction::ForkFrom, .. } ) } pub fn is_rewind_menu(&self) -> bool { matches!( self.mode, InputSuggestionsMode::UserQueryMenu { action: super::UserQueryMenuAction::Rewind, .. } ) } /// Returns the conversation_id if the current mode is UserQueryMenu (ForkFrom). pub fn user_query_conversation_id(&self) -> Option { match &self.mode { InputSuggestionsMode::UserQueryMenu { action: super::UserQueryMenuAction::ForkFrom, conversation_id, } => Some(*conversation_id), _ => None, } } /// Returns the conversation_id if the current mode is RewindMenu. pub fn rewind_conversation_id(&self) -> Option { match &self.mode { InputSuggestionsMode::UserQueryMenu { action: super::UserQueryMenuAction::Rewind, conversation_id, } => Some(*conversation_id), _ => None, } } pub fn is_inline_history_menu(&self) -> bool { matches!(self.mode, InputSuggestionsMode::InlineHistoryMenu { .. }) } pub fn is_repos_menu(&self) -> bool { matches!(self.mode, InputSuggestionsMode::IndexedReposMenu) } pub fn is_plan_menu(&self) -> bool { matches!(self.mode, InputSuggestionsMode::PlanMenu { .. }) } /// Returns the conversation_id if the current mode is PlanMenu. pub fn plan_menu_conversation_id(&self) -> Option { match &self.mode { InputSuggestionsMode::PlanMenu { conversation_id } => Some(*conversation_id), _ => None, } } pub fn inline_menu_type(&self) -> Option { InlineMenuType::from_suggestions_mode(&self.mode) } pub fn is_inline_menu_open(&self) -> bool { self.mode.is_inline_menu() } } impl Entity for InputSuggestionsModeModel { type Event = InputSuggestionsModeEvent; } pub enum InputSuggestionsModeEvent { ModeChanged { /// The saved buffer state to restore, if this mode change is an inline menu closing. /// `None` for all other transitions. buffer_to_restore: Option, /// The saved input config to restore, if this mode change closes inline history menu /// without accepting the temporary preview state. input_config_to_restore: Option, }, }