//! Model-layer AI input state management logic. //! //! The primary export of this module is `BlocklistAIInputModel`, which is a terminal-surface-scoped //! model managing input "type" state (whether the input is in AI or shell mode). This model also //! exposes methods for running query autodetection, where an algorithm determines if the current //! input contents are an AI query or shell command, which is then used to update the input mode. use std::sync::Arc; use futures::stream::AbortHandle; use galaxy_completer::completer::CompletionContext; use galaxy_core::features::FeatureFlag; use galaxyui::{AppContext, Entity, EntityId, ModelContext, ModelHandle, SingletonEntity}; use input_classifier::util::{is_agent_follow_up_input, is_one_off_natural_language_word}; pub use input_classifier::{InputClassifierDecisionSource, InputType}; use instant::Instant; use parking_lot::FairMutex; use serde::{Deserialize, Serialize}; use session_sharing_protocol::common::{InputMode, InputType as ProtocolInputType}; use settings::Setting as _; /// The source of the final input type decision applied to the user input. #[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum InputTypeAutoDetectionSource { /// Decision produced by the input classifier pipeline. InputClassifierDecisionSource(InputClassifierDecisionSource), /// User explicitly toggled the input type via cmd + I. ManualToggle, /// `!` shell prefix force-locked the input to Shell mode. ShellPrefix, /// Image / file attachment in progress force-locked AI mode. AttachmentForcedAi, /// First token matched the autodetection command denylist. Denylist, /// Buffer text closely matched a recent shell history entry. HistoryMatch, /// Input matched the natural-language follow-up allowlist after a preceding AI block. NaturalLanguageAgentFollowUpAllowList, /// Inline history menu / history-up suggestion selection set the input type. HistorySelection, /// Inserting a workflow into the input set the input type based on workflow kind. WorkflowInsertion, /// Accepting a shell command autosuggestion forced Shell mode. CommandAutosuggestionAccepted, /// Accepting an Agent Mode query autosuggestion forced AI mode. AgentQueryAutosuggestionAccepted, /// Empty-buffer conversation context render forced AI so conversation context renders. ConversationContextRender, /// "Continue conversation" button forced AI mode. ContinueConversation, /// Onboarding tutorial agent prompt forced AI mode. OnboardingAgentPrompt, /// Starting a new agent conversation forced AI mode. StartNewConversation, /// Ask-AI flow (text/block selection, programmatic Ask-AI lock) forced AI mode. AskAi, /// Detected/composing slash or skill command forced AI mode. SlashCommand, /// Entering inline agent view force-locked AI without an explicit user toggle. InlineAgentViewEntry, /// Activating cloud handoff compose (`&` prefix or programmatic) force-locked AI. CloudHandoffEnter, /// Exiting cloud handoff compose restored AI / unlocked-if-autodetect. CloudHandoffExit, /// Legacy non-AgentView `?` AI prefix path force-locked AI. AgentModePrefix, /// Inline code review send overrode the input mode to AI. InlineCodeReviewSend, /// External input config update from session sharing applied. SessionSharingApply, /// Fullscreen AgentView inline history command cycling force-locked Shell. FullscreenInlineHistoryCycling, /// Closing history suggestions restored the previously saved config. RestoreSavedConfig, /// `set_input_config_for_classic_mode` reset (CtrlC, delete-all-left, etc.). ClassicModeReset, /// Toggling voice input forced AI mode. VoiceInputToggle, /// Inserting from the AI `@` context menu forced AI mode. AtContextMenuInsert, } impl From for InputTypeAutoDetectionSource { fn from(value: InputClassifierDecisionSource) -> Self { Self::InputClassifierDecisionSource(value) } } use super::agent_view::AgentViewEntryOrigin; use super::context_model::BlocklistAIContextModel; use super::telemetry_banner::should_collect_ai_ugc_telemetry; use super::{ConversationSelectionEvent, ConversationSelectionHandle}; use crate::input_classifier::InputClassifierModel; use crate::settings::{AISettings, AISettingsChangedEvent, InputBoxType, InputSettings}; use crate::terminal::cli_agent_sessions::{ CLIAgentInputState, CLIAgentSessionsModel, CLIAgentSessionsModelEvent, }; use crate::terminal::input::decorations::ParsedTokensSnapshot; use crate::terminal::model::rich_content::RichContentType; use crate::terminal::model::session::SessionId; use crate::terminal::{History, TerminalModel}; use crate::{report_if_error, send_telemetry_from_ctx, PrivacySettings, TelemetryEvent}; /// Cutoff score for deciding an user input matches a history command entry. const HISTORY_ENTRY_MATCH_CUTOFF: f32 = 0.9; /// Duration to temporarily disable autodetection during operations like history selection. const AUTODETECTION_DISABLE_DURATION_MS: u64 = 250; /// Configuration for the terminal pane's input. #[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct InputConfig { /// The type of the terminal input. pub input_type: InputType, /// If `true`, we will not attempt to auto-detect the best input type. pub is_locked: bool, } impl InputConfig { /// Create a sensible default InputConfig based on user's auto-detection setting. pub fn new(app: &AppContext) -> Self { let ai_settings = AISettings::as_ref(app); let is_autodetection_enabled = ai_settings.is_ai_autodetection_enabled(app); InputConfig { input_type: InputType::Shell, is_locked: !is_autodetection_enabled, // Locked if auto-detection disabled } } pub fn with_toggled_type(self) -> Self { let input_type = if self.input_type.is_ai() { InputType::Shell } else { InputType::AI }; Self { input_type, ..self } } pub fn with_shell_type(self) -> Self { Self { input_type: InputType::Shell, ..self } } pub fn with_input_type(self, input_type: InputType) -> Self { Self { input_type, ..self } } pub fn unlocked_if_autodetection_enabled( self, is_in_fullscreen_agent_view: bool, app: &AppContext, ) -> Self { Self { is_locked: if !FeatureFlag::AgentView.is_enabled() || is_in_fullscreen_agent_view { !AISettings::as_ref(app).is_ai_autodetection_enabled(app) } else { !AISettings::as_ref(app).is_nld_in_terminal_enabled(app) }, ..self } } pub fn locked(self) -> Self { Self { is_locked: true, ..self } } pub fn is_ai(&self) -> bool { self.input_type == InputType::AI } pub fn is_shell(&self) -> bool { self.input_type == InputType::Shell } } impl From for InputMode { fn from(config: InputConfig) -> Self { let protocol_input_type = match config.input_type { InputType::Shell => ProtocolInputType::Shell, InputType::AI => ProtocolInputType::AI, }; InputMode::new(protocol_input_type, config.is_locked) } } /// Terminal-surface-scoped model responsible for managing AI input state. #[derive(Clone)] pub struct BlocklistAIInputModel { input_config: InputConfig, /// The timestamp of the last time the input mode was switched, if the switch was to AI mode and /// it was autodetected. Else, `None`. last_ai_autodetection_ts: Option, /// the latest input type classification decision source last_ai_autodetection_source: Option, /// Timestamp of the last time the input type was explicitly set. last_explicit_input_type_set_at: Option, /// Whether the input buffer was empty at the time the lock was set. This will be true /// if a persistent lock is in place and a buffer is submitted. was_lock_set_with_empty_buffer: bool, conversation_selection: ConversationSelectionHandle, /// Handle to the per-surface context model. Used to read pending image / file attachments /// when deciding whether to force-lock the input to AI mode (see /// [`BlocklistAIContextModel::has_locking_attachment`]). ai_context_model: ModelHandle, terminal_surface_id: EntityId, autodetect_abort_handle: Option, model: Arc>, } impl BlocklistAIInputModel { /// Creates input state for a terminal surface. pub fn new( model: Arc>, conversation_selection: ConversationSelectionHandle, ai_context_model: ModelHandle, terminal_surface_id: EntityId, ctx: &mut ModelContext, ) -> Self { // Reactively restore input config when CLI agent rich input closes. ctx.subscribe_to_model( &CLIAgentSessionsModel::handle(ctx), move |me, _, event, ctx| { let CLIAgentSessionsModelEvent::InputSessionChanged { terminal_view_id: event_view_id, previous_input_state, .. } = event else { return; }; // CLI agent sessions are keyed by terminal view id; GUI surfaces use the // view id as their surface id, so this filters events to our surface. if *event_view_id != terminal_surface_id { return; } if let CLIAgentInputState::Open { previous_input_config, previous_was_lock_set_with_empty_buffer, .. } = previous_input_state { me.restore_input_config( *previous_input_config, *previous_was_lock_set_with_empty_buffer, ctx, ); } }, ); ctx.subscribe_to_model(&AISettings::handle(ctx), move |me, _, event, ctx| { match event { AISettingsChangedEvent::AIAutoDetectionEnabled { .. } if FeatureFlag::AgentView.is_enabled() => { if me.is_conversation_fullscreen(ctx) { // Use context-specific check to determine if autodetection should be enabled let is_nld_enabled = AISettings::as_ref(ctx).is_ai_autodetection_enabled(ctx); // If autodetection is enabled, unlock the input. me.set_input_config_internal( InputConfig { is_locked: !is_nld_enabled, input_type: InputType::AI, }, None, ctx, ); } } AISettingsChangedEvent::AIAutoDetectionEnabled { .. } => { // Use context-specific check to determine if autodetection should be enabled let is_autodetection_enabled = me.is_autodetection_enabled_for_current_context(ctx); // If autodetection is enabled, unlock the input. me.set_input_config_internal( InputConfig { is_locked: !is_autodetection_enabled, ..me.input_config() }, None, ctx, ); } AISettingsChangedEvent::NLDInTerminalEnabled { .. } if FeatureFlag::AgentView.is_enabled() && !me.is_conversation_active(ctx) => { let is_nld_enabled = AISettings::as_ref(ctx).is_nld_in_terminal_enabled(ctx); me.set_input_config_internal( InputConfig { is_locked: !is_nld_enabled, input_type: InputType::Shell, }, None, ctx, ); } _ => (), } }); ctx.subscribe_to_model(&conversation_selection, |me, _, event, ctx| match event { ConversationSelectionEvent::Activated { is_fullscreen, origin, } => { if !*is_fullscreen { me.set_input_config_internal( InputConfig { input_type: InputType::AI, is_locked: true, }, Some(InputTypeAutoDetectionSource::InlineAgentViewEntry), ctx, ); } else if matches!(origin, AgentViewEntryOrigin::ClearBuffer) { let is_autodetection_enabled = AISettings::as_ref(ctx).is_ai_autodetection_enabled(ctx); me.set_input_config_internal( InputConfig { input_type: me.input_config().input_type, is_locked: !is_autodetection_enabled, }, None, ctx, ); } else if me.has_locking_attachment(ctx) { me.set_input_config_internal( InputConfig { input_type: InputType::AI, is_locked: true, }, Some(InputTypeAutoDetectionSource::AttachmentForcedAi), ctx, ); } else { let is_autodetection_enabled = AISettings::as_ref(ctx).is_ai_autodetection_enabled(ctx); if is_autodetection_enabled { me.temporarily_disable_autodetection(); } me.set_input_config_internal( InputConfig { input_type: InputType::AI, is_locked: !is_autodetection_enabled, }, None, ctx, ); } } ConversationSelectionEvent::Deactivated { is_exit_before_new_entrance, .. } => { if !is_exit_before_new_entrance { let is_nld_in_terminal_enabled = AISettings::as_ref(ctx).is_nld_in_terminal_enabled(ctx); me.set_input_config_internal( InputConfig { input_type: InputType::Shell, is_locked: !is_nld_in_terminal_enabled, }, None, ctx, ); } } ConversationSelectionEvent::Changed => {} }); let is_autodetection_enabled = if FeatureFlag::AgentView.is_enabled() { AISettings::as_ref(ctx).is_nld_in_terminal_enabled(ctx) } else { AISettings::as_ref(ctx).is_ai_autodetection_enabled(ctx) }; let initial_decision_source = None; Self { input_config: InputConfig { input_type: InputType::Shell, is_locked: !is_autodetection_enabled, }, conversation_selection, ai_context_model, terminal_surface_id, last_ai_autodetection_ts: None, last_ai_autodetection_source: initial_decision_source, last_explicit_input_type_set_at: None, was_lock_set_with_empty_buffer: false, autodetect_abort_handle: None, model, } } /// Returns whether the surface presents a selected conversation as active. fn is_conversation_active(&self, app: &AppContext) -> bool { self.conversation_selection .as_ref(app) .is_conversation_active(app) } /// Returns whether the surface presents a selected conversation fullscreen. fn is_conversation_fullscreen(&self, app: &AppContext) -> bool { self.conversation_selection .as_ref(app) .is_conversation_fullscreen(app) } /// Convenience wrapper around `BlocklistAIContextModel::has_locking_attachment`. fn has_locking_attachment(&self, app: &AppContext) -> bool { self.ai_context_model.as_ref(app).has_locking_attachment() } /// Returns the InputType enum which specifies how we will handle the terminal input. pub fn input_type(&self) -> InputType { self.input_config.input_type } /// Whether the input type is locked. Does not take user autodetection setting or feature flags /// into account. pub fn is_input_type_locked(&self) -> bool { self.input_config.is_locked } pub fn is_ai_input_enabled(&self) -> bool { matches!(self.input_config.input_type, InputType::AI) } pub fn input_config(&self) -> InputConfig { self.input_config } pub fn last_ai_autodetection_source(&self) -> Option { self.last_ai_autodetection_source } pub fn last_ai_autodetection_ts(&self) -> Option { self.last_ai_autodetection_ts } /// Sets the input config iff the input is in classic mode (i.e. not UDI). pub fn set_input_config_for_classic_mode( &mut self, new_config: InputConfig, ctx: &mut ModelContext, ) { // When agent view is active, the input should behave like Universal mode // even if Classic mode is selected (e.g. when PS1 is enabled). if FeatureFlag::AgentView.is_enabled() && self.is_conversation_active(ctx) { return; } let input_type = InputSettings::as_ref(ctx).input_type(ctx); if !matches!(input_type, InputBoxType::Classic) { return; } self.set_input_config_internal( new_config, Some(InputTypeAutoDetectionSource::ClassicModeReset), ctx, ); } /// Swaps between Agent/Shell input types while preserving lock state. Temporarily disables /// autodetection. pub fn set_input_type( &mut self, input_type: InputType, decision_source: Option, ctx: &mut ModelContext, ) { self.temporarily_disable_autodetection(); let current_config = self.input_config(); self.set_input_config_internal( current_config.with_input_type(input_type), decision_source, ctx, ); } /// Does not disable autodetection. fn set_input_config_internal( &mut self, new_config: InputConfig, decision_source: Option, ctx: &mut ModelContext, ) -> bool { // When `AgentView` is enabled, AI input mode can only be set in the top-level terminal // mode via autodetection; it cannot be locked to AI input mode unless there is an active // agent view or a CLI agent rich input session is open. In the agent view case, executing // autodetected AI input will trigger entering the agent view with that query. In the CLI // agent rich input case, the input must be in AI mode to suppress shell decorations // (syntax highlighting, error underlining). if FeatureFlag::AgentView.is_enabled() && !self.is_conversation_active(ctx) && new_config.input_type.is_ai() && new_config.is_locked && !CLIAgentSessionsModel::as_ref(ctx).is_input_open(self.terminal_surface_id) { return false; } if self.input_config == new_config { self.last_ai_autodetection_source = decision_source; return false; } let old_config = self.input_config; if !new_config.is_locked && new_config.input_type.is_ai() { self.last_ai_autodetection_ts = Some(Instant::now()); } else { self.last_ai_autodetection_ts = None; } if new_config.input_type.is_ai() { AISettings::handle(ctx).update(ctx, |settings, ctx| { let new_num_times = *settings.entered_agent_mode_num_times + 1; report_if_error!(settings .entered_agent_mode_num_times .set_value(new_num_times, ctx)); }); } self.input_config = new_config; self.last_ai_autodetection_source = decision_source; // Emit specific events for what actually changed if old_config.input_type != new_config.input_type { ctx.emit(BlocklistAIInputEvent::InputTypeChanged { config: new_config }); } if old_config.is_locked != new_config.is_locked { ctx.emit(BlocklistAIInputEvent::LockChanged { config: new_config }); } true } /// Allows you to set the input config and mutate the lock state. Temporarily disables autodetection. pub fn set_input_config( &mut self, new_config: InputConfig, is_input_buffer_empty: bool, decision_source: Option, ctx: &mut ModelContext, ) { self.temporarily_disable_autodetection(); self.set_input_config_internal(new_config, decision_source, ctx); if new_config.is_locked { self.abort_in_progress_detection(); } self.was_lock_set_with_empty_buffer = self.is_input_type_locked() && is_input_buffer_empty; } /// Restores a previous input config without recomputing whether the lock was set while the /// buffer was empty. fn restore_input_config( &mut self, new_config: InputConfig, was_lock_set_with_empty_buffer: bool, ctx: &mut ModelContext, ) { self.temporarily_disable_autodetection(); self.set_input_config_internal(new_config, None, ctx); self.abort_in_progress_detection(); self.was_lock_set_with_empty_buffer = was_lock_set_with_empty_buffer; } /// Returns `false` if the input type is locked and we will not attempt to automatically detect /// and change the input type. pub fn should_run_input_autodetection(&self, app: &AppContext) -> bool { FeatureFlag::AgentMode.is_enabled() && self.is_autodetection_enabled_for_current_context(app) && !self.input_config.is_locked } /// Returns whether autodetection is enabled for the current context. /// When AgentView is enabled, this checks whether we're in agent view or terminal mode /// and returns the appropriate setting. pub fn is_autodetection_enabled_for_current_context(&self, app: &AppContext) -> bool { // If the agent is in control or tagged in, don't run autodetection. if self .model .lock() .block_list() .active_block() .is_agent_in_control_or_tagged_in() { return false; } // Defense in depth: while there is a pending image / file attachment, the classifier // must never have a chance to flip the input back to shell mode, even per-keystroke. // The conversation-activation subscriber and `set_input_mode_agent` already lock at entry; // this guard protects the window if any future caller forgets. if self.has_locking_attachment(app) { return false; } let ai_settings = AISettings::as_ref(app); if FeatureFlag::AgentView.is_enabled() { if self.is_conversation_fullscreen(app) { ai_settings.is_ai_autodetection_enabled(app) } else { ai_settings.is_nld_in_terminal_enabled(app) } } else { // AgentView not enabled: use the main autodetection setting ai_settings.is_ai_autodetection_enabled(app) } } /// Temporarily disable autodetection for a fixed duration. /// Useful for operations like history selection where we don't want /// autodetection to interfere with the manual input type setting. fn temporarily_disable_autodetection(&mut self) { self.last_explicit_input_type_set_at = Some(Instant::now()); } pub fn enable_autodetection(&mut self, input_type: InputType, ctx: &mut ModelContext) { self.set_input_config_internal( InputConfig { input_type, is_locked: false, }, None, ctx, ); // The goal of this function is to allow autodetection to run, but if we // don't clear this, it may be suppressed for a short duration. self.last_explicit_input_type_set_at = None; } /// Handles the input buffer being submitted. pub fn handle_input_buffer_submitted(&mut self, ctx: &mut ModelContext) { // If the agent is still in control of a long-running command, keep the input locked to AI mode. let is_agent_in_control_or_tagged_in = self .model .lock() .block_list() .active_block() .is_agent_in_control_or_tagged_in(); let new_config = if is_agent_in_control_or_tagged_in { InputConfig { input_type: InputType::AI, is_locked: true, } } else { // If NLD is enabled and input is currently locked, unlock it, as we want to // resume autodetection for the next input. self.input_config .unlocked_if_autodetection_enabled(self.is_conversation_fullscreen(ctx), ctx) }; self.set_input_config( new_config, // We know the buffer is currently empty, as it was just submitted. true, None, ctx, ); } pub fn was_lock_set_with_empty_buffer(&self) -> bool { self.was_lock_set_with_empty_buffer } /// Aborts any in progress work for autodetection. pub fn abort_in_progress_detection(&mut self) { if let Some(handle) = self.autodetect_abort_handle.take() { handle.abort(); } } /// If the input type is unlocked, analyze the input and set the input type to the type we /// detected. Emits an event if the input mode changed. If the input mode is locked, do /// nothing. /// /// When `session_id` is `Some`, history matching is performed. The `completion_context` /// is always used for alias expansion (callers without a live session should pass an /// `EmptyCompletionContext`). pub fn detect_and_set_input_type( &mut self, input: ParsedTokensSnapshot, completion_context: C, session_id: Option, ctx: &mut ModelContext, ) { // Abort the last autodetect handle if exists. self.abort_in_progress_detection(); // If the input mode is locked, there's no point in running autodetection. if !self.should_run_input_autodetection(ctx) { return; } if self .last_explicit_input_type_set_at .map(|last_explicitly_set_at| { Instant::now() < last_explicitly_set_at + std::time::Duration::from_millis(AUTODETECTION_DISABLE_DURATION_MS) }) .unwrap_or(false) { return; } let first_token_str = input.parsed_tokens.first().map(|t| t.token.clone()); let Some(first_token_str) = first_token_str else { // If the buffer is empty, short-circuit and leave input type unchanged. // // We don't know enough (anything) to be able to change the input type one way or // another. return; }; let denylist: Vec<&str> = AISettings::as_ref(ctx) .autodetection_command_denylist .value() .split(',') .collect(); // Early return if the first token is included in the denylist. No need to parse history. if denylist.contains(&first_token_str.as_str()) { self.set_input_config_internal( InputConfig { input_type: InputType::Shell, ..self.input_config() }, Some(InputTypeAutoDetectionSource::Denylist), ctx, ); return; } // If we have a session, gather history entries for matching. let history_entries = session_id.map(|sid| { History::as_ref(ctx) .commands(sid) .into_iter() .flatten() .filter_map(|entry| { if entry .exit_code .is_some_and(|code| code.was_command_not_found()) { return None; } Some(entry.command.to_string()) }) .collect::>() }); let buffer_cloned = input.buffer_text.clone(); let other_buffer_cloned = buffer_cloned.clone(); let current_input_type = self.input_type(); let is_udi_enabled = InputSettings::as_ref(ctx).is_universal_developer_input_enabled(ctx); // Determine if the input is a follow-up to an AI block. let is_agent_follow_up = { let model = self.model.lock(); let block_list = model.block_list(); let block_index = block_list.last_non_hidden_block_by_index(); match block_list.last_non_hidden_rich_content_block_after_block(block_index) { Some((_, content)) => content.content_type == Some(RichContentType::AIBlock), _ => false, } }; let classifier = InputClassifierModel::as_ref(ctx).classifier(); let handle = ctx .spawn( async move { // First check if the token is a natural language word, if current input type is AI if matches!(current_input_type, InputType::AI) && is_one_off_natural_language_word(first_token_str.to_lowercase().as_str()) { return ( InputType::AI, InputClassifierDecisionSource::NaturalLanguageOneOffAllowlist.into(), ); } // If this is clearly intended to be a follow-up to an AI block, classify it as AI. if is_agent_follow_up && is_agent_follow_up_input(&buffer_cloned.trim().to_lowercase()) { return ( InputType::AI, InputTypeAutoDetectionSource::NaturalLanguageAgentFollowUpAllowList, ); } // If we have history entries (i.e., a live session), check for // close matches to short-circuit as shell input. if let Some(history_entries) = history_entries { if has_any_close_matches( &buffer_cloned, history_entries.iter().map(AsRef::as_ref), HISTORY_ENTRY_MATCH_CUTOFF, ) .await { return (InputType::Shell, InputTypeAutoDetectionSource::HistoryMatch); } } // Yield so that an attempt to abort the classification is handled. We do this periodically // so that we can skip doing additional expensive work if the classification is aborted. futures_lite::future::yield_now().await; let input = galaxy_completer::util::expand_aliases(input, &completion_context).await; futures_lite::future::yield_now().await; let context = input_classifier::Context { current_input_type, is_agent_follow_up, }; let classification = classifier.detect_input_type(input.clone(), &context).await; futures_lite::future::yield_now().await; (classification.input_type, classification.source.into()) }, move |me, (new_input_type, decision_source), ctx| { // In theory, we shouldn't need to check this, as we only run autodetection if the input // is not locked, and we should abort the autodetect future if the input is locked, but // we do it anyway out of an abundance of caution. if !me.should_run_input_autodetection(ctx) { return; } // If the autodetect abort handle is none, it means we aborted autodetection. // It's possible that the future already completed before we aborted, and then we reach this callback after abort. // In this case, don't set the input type. if me.autodetect_abort_handle.is_none() { return; } me.set_input_config_internal( InputConfig { input_type: new_input_type, ..me.input_config() }, Some(decision_source), ctx, ); if current_input_type != new_input_type { let buffer_length = other_buffer_cloned.len(); let input_buffer_text_for_telemetry = should_collect_ai_ugc_telemetry( ctx, PrivacySettings::as_ref(ctx).is_telemetry_enabled, ) .then_some(other_buffer_cloned); send_telemetry_from_ctx!( TelemetryEvent::AgentModeChangedInputType { input: input_buffer_text_for_telemetry, buffer_length, is_manually_changed: false, new_input_type, active_block_id: me .model .lock() .block_list() .active_block_id() .clone(), is_udi_enabled, }, ctx ); } }, ) .abort_handle(); self.autodetect_abort_handle = Some(handle); } } #[derive(Debug, Clone)] pub enum BlocklistAIInputEvent { /// Emitted when the terminal input type is updated. InputTypeChanged { /// The new input config. config: InputConfig, }, /// Emitted when the input lock state is updated. LockChanged { /// The new input config. config: InputConfig, }, } impl BlocklistAIInputEvent { pub fn did_update_input_config(&self) -> bool { match self { BlocklistAIInputEvent::InputTypeChanged { .. } | BlocklistAIInputEvent::LockChanged { .. } => true, } } pub fn updated_config(&self) -> &InputConfig { match self { BlocklistAIInputEvent::InputTypeChanged { config } | BlocklistAIInputEvent::LockChanged { config } => config, } } } impl Entity for BlocklistAIInputModel { type Event = BlocklistAIInputEvent; } /// Returns whether the set of possibilities contains any close matches /// to the provided word, using the given similarity threshold. /// /// Adapted from [`difflib::get_close_matches`], but an async function with /// periodic yields such that the operation can be aborted if necessary. Also, /// unlike the original function, this returns as soon as it finds any match /// above the threshold, instead of finding _all_ matches above the threshold /// and returning the top N matches. async fn has_any_close_matches<'a>( word: &str, possibilities: impl Iterator, cutoff: f32, ) -> bool { const BATCH_SIZE: usize = 50; if !(0.0..=1.0).contains(&cutoff) { panic!("Cutoff must be greater than 0.0 and lower than 1.0"); } let mut matcher = difflib::sequencematcher::SequenceMatcher::new("", word); for (idx, i) in possibilities.enumerate() { // Periodically, yield to the executor so this task can be aborted if // requested. if idx % BATCH_SIZE == 0 { futures_lite::future::yield_now().await; } matcher.set_first_seq(i); // The fast ratio computations produce an upper bound on the value of // ratio, so if a faster check fails, the slower checks are guaranteed // to also fail. if matcher.real_quick_ratio() >= cutoff && matcher.ratio() >= cutoff { return true; } } false }