first pass of merging in warp (doesn't build)
This commit is contained in:
+249
-118
@@ -1,6 +1,6 @@
|
||||
//! Model-layer AI input state management logic.
|
||||
//!
|
||||
//! The primary export of this module is `BlocklistAIInputModel`, which is a terminal pane-scoped
|
||||
//! 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.
|
||||
@@ -11,34 +11,95 @@ use futures::stream::AbortHandle;
|
||||
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 _;
|
||||
use galaxy_completer::completer::CompletionContext;
|
||||
|
||||
pub use input_classifier::InputType;
|
||||
/// 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,
|
||||
}
|
||||
|
||||
use super::agent_view::{AgentViewController, AgentViewControllerEvent, AgentViewEntryOrigin};
|
||||
impl From<InputClassifierDecisionSource> 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::PrivacySettings;
|
||||
use galaxy_completer::completer::CompletionContext;
|
||||
|
||||
use crate::{
|
||||
input_classifier::InputClassifierModel,
|
||||
report_if_error, send_telemetry_from_ctx,
|
||||
settings::{AISettings, AISettingsChangedEvent, InputBoxType, InputSettings},
|
||||
terminal::{
|
||||
input::decorations::ParsedTokensSnapshot,
|
||||
model::{rich_content::RichContentType, session::SessionId},
|
||||
History, TerminalModel,
|
||||
},
|
||||
TelemetryEvent,
|
||||
};
|
||||
|
||||
use super::telemetry_banner::should_collect_ai_ugc_telemetry;
|
||||
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;
|
||||
@@ -130,7 +191,7 @@ impl From<InputConfig> for InputMode {
|
||||
}
|
||||
}
|
||||
|
||||
/// Terminal pane-scoped model responsible for managing AI input state.
|
||||
/// Terminal-surface-scoped model responsible for managing AI input state.
|
||||
#[derive(Clone)]
|
||||
pub struct BlocklistAIInputModel {
|
||||
input_config: InputConfig,
|
||||
@@ -138,6 +199,8 @@ pub struct BlocklistAIInputModel {
|
||||
/// 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<Instant>,
|
||||
/// the latest input type classification decision source
|
||||
last_ai_autodetection_source: Option<InputTypeAutoDetectionSource>,
|
||||
|
||||
/// Timestamp of the last time the input type was explicitly set.
|
||||
last_explicit_input_type_set_at: Option<Instant>,
|
||||
@@ -146,25 +209,32 @@ pub struct BlocklistAIInputModel {
|
||||
/// if a persistent lock is in place and a buffer is submitted.
|
||||
was_lock_set_with_empty_buffer: bool,
|
||||
|
||||
agent_view_controller: ModelHandle<AgentViewController>,
|
||||
conversation_selection: ConversationSelectionHandle,
|
||||
|
||||
terminal_view_id: EntityId,
|
||||
/// 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<BlocklistAIContextModel>,
|
||||
|
||||
terminal_surface_id: EntityId,
|
||||
|
||||
autodetect_abort_handle: Option<AbortHandle>,
|
||||
model: Arc<FairMutex<TerminalModel>>,
|
||||
}
|
||||
|
||||
impl BlocklistAIInputModel {
|
||||
/// Creates input state for a terminal surface.
|
||||
pub fn new(
|
||||
model: Arc<FairMutex<TerminalModel>>,
|
||||
agent_view_controller: ModelHandle<AgentViewController>,
|
||||
terminal_view_id: EntityId,
|
||||
conversation_selection: ConversationSelectionHandle,
|
||||
ai_context_model: ModelHandle<BlocklistAIContextModel>,
|
||||
terminal_surface_id: EntityId,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
// Reactively restore input config when CLI agent rich input closes.
|
||||
ctx.subscribe_to_model(
|
||||
&CLIAgentSessionsModel::handle(ctx),
|
||||
move |me, event, ctx| {
|
||||
move |me, _, event, ctx| {
|
||||
let CLIAgentSessionsModelEvent::InputSessionChanged {
|
||||
terminal_view_id: event_view_id,
|
||||
previous_input_state,
|
||||
@@ -173,7 +243,9 @@ impl BlocklistAIInputModel {
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if *event_view_id != terminal_view_id {
|
||||
// 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 {
|
||||
@@ -191,12 +263,12 @@ impl BlocklistAIInputModel {
|
||||
},
|
||||
);
|
||||
|
||||
ctx.subscribe_to_model(&AISettings::handle(ctx), move |me, event, ctx| {
|
||||
ctx.subscribe_to_model(&AISettings::handle(ctx), move |me, _, event, ctx| {
|
||||
match event {
|
||||
AISettingsChangedEvent::AIAutoDetectionEnabled { .. }
|
||||
if FeatureFlag::AgentView.is_enabled() =>
|
||||
{
|
||||
if me.agent_view_controller.as_ref(ctx).is_fullscreen() {
|
||||
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);
|
||||
@@ -207,6 +279,7 @@ impl BlocklistAIInputModel {
|
||||
is_locked: !is_nld_enabled,
|
||||
input_type: InputType::AI,
|
||||
},
|
||||
None,
|
||||
ctx,
|
||||
);
|
||||
}
|
||||
@@ -222,12 +295,12 @@ impl BlocklistAIInputModel {
|
||||
is_locked: !is_autodetection_enabled,
|
||||
..me.input_config()
|
||||
},
|
||||
None,
|
||||
ctx,
|
||||
);
|
||||
}
|
||||
AISettingsChangedEvent::NLDInTerminalEnabled { .. }
|
||||
if FeatureFlag::AgentView.is_enabled()
|
||||
&& !me.agent_view_controller.as_ref(ctx).is_active() =>
|
||||
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(
|
||||
@@ -235,6 +308,7 @@ impl BlocklistAIInputModel {
|
||||
is_locked: !is_nld_enabled,
|
||||
input_type: InputType::Shell,
|
||||
},
|
||||
None,
|
||||
ctx,
|
||||
);
|
||||
}
|
||||
@@ -242,86 +316,92 @@ impl BlocklistAIInputModel {
|
||||
}
|
||||
});
|
||||
|
||||
if FeatureFlag::AgentView.is_enabled() {
|
||||
ctx.subscribe_to_model(&agent_view_controller, |me, event, ctx| match event {
|
||||
AgentViewControllerEvent::EnteredAgentView {
|
||||
display_mode,
|
||||
origin,
|
||||
..
|
||||
} => {
|
||||
if display_mode.is_inline() {
|
||||
me.set_input_config_internal(
|
||||
InputConfig {
|
||||
input_type: InputType::AI,
|
||||
is_locked: true,
|
||||
},
|
||||
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,
|
||||
},
|
||||
ctx,
|
||||
);
|
||||
} else {
|
||||
let is_autodetection_enabled =
|
||||
AISettings::as_ref(ctx).is_ai_autodetection_enabled(ctx);
|
||||
if is_autodetection_enabled {
|
||||
// Upon entering the agent view, temporarily disable autodetection as
|
||||
// the existing buffer contents, if any are now most likely intended to
|
||||
// be sent to the agent, and if the input would otherwise trigger a
|
||||
// false-negative classification, we'd drop the user right into shell
|
||||
// mode.
|
||||
me.temporarily_disable_autodetection();
|
||||
}
|
||||
me.set_input_config_internal(
|
||||
InputConfig {
|
||||
input_type: InputType::AI,
|
||||
is_locked: !is_autodetection_enabled,
|
||||
},
|
||||
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,
|
||||
);
|
||||
}
|
||||
AgentViewControllerEvent::ExitedAgentView {
|
||||
is_exit_before_new_entrance,
|
||||
..
|
||||
} => {
|
||||
if !is_exit_before_new_entrance {
|
||||
// When truly exiting agent view, use the terminal-specific NLD setting
|
||||
// since the user is returning to terminal mode.
|
||||
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,
|
||||
},
|
||||
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,
|
||||
},
|
||||
agent_view_controller,
|
||||
terminal_view_id,
|
||||
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,
|
||||
@@ -329,6 +409,25 @@ impl BlocklistAIInputModel {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
@@ -347,6 +446,9 @@ impl BlocklistAIInputModel {
|
||||
pub fn input_config(&self) -> InputConfig {
|
||||
self.input_config
|
||||
}
|
||||
pub fn last_ai_autodetection_source(&self) -> Option<InputTypeAutoDetectionSource> {
|
||||
self.last_ai_autodetection_source
|
||||
}
|
||||
|
||||
pub fn last_ai_autodetection_ts(&self) -> Option<Instant> {
|
||||
self.last_ai_autodetection_ts
|
||||
@@ -360,8 +462,7 @@ impl BlocklistAIInputModel {
|
||||
) {
|
||||
// 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.agent_view_controller.as_ref(ctx).is_active()
|
||||
{
|
||||
if FeatureFlag::AgentView.is_enabled() && self.is_conversation_active(ctx) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -369,21 +470,35 @@ impl BlocklistAIInputModel {
|
||||
if !matches!(input_type, InputBoxType::Classic) {
|
||||
return;
|
||||
}
|
||||
self.set_input_config_internal(new_config, ctx);
|
||||
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, ctx: &mut ModelContext<Self>) {
|
||||
pub fn set_input_type(
|
||||
&mut self,
|
||||
input_type: InputType,
|
||||
decision_source: Option<InputTypeAutoDetectionSource>,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) {
|
||||
self.temporarily_disable_autodetection();
|
||||
let current_config = self.input_config();
|
||||
self.set_input_config_internal(current_config.with_input_type(input_type), ctx);
|
||||
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<InputTypeAutoDetectionSource>,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) -> bool {
|
||||
// When `AgentView` is enabled, AI input mode can only be set in the top-level terminal
|
||||
@@ -393,15 +508,16 @@ impl BlocklistAIInputModel {
|
||||
// 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.agent_view_controller.as_ref(ctx).is_active()
|
||||
&& !self.is_conversation_active(ctx)
|
||||
&& new_config.input_type.is_ai()
|
||||
&& new_config.is_locked
|
||||
&& !CLIAgentSessionsModel::as_ref(ctx).is_input_open(self.terminal_view_id)
|
||||
&& !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;
|
||||
}
|
||||
|
||||
@@ -423,6 +539,7 @@ impl BlocklistAIInputModel {
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -441,10 +558,11 @@ impl BlocklistAIInputModel {
|
||||
&mut self,
|
||||
new_config: InputConfig,
|
||||
is_input_buffer_empty: bool,
|
||||
decision_source: Option<InputTypeAutoDetectionSource>,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) {
|
||||
self.temporarily_disable_autodetection();
|
||||
self.set_input_config_internal(new_config, ctx);
|
||||
self.set_input_config_internal(new_config, decision_source, ctx);
|
||||
if new_config.is_locked {
|
||||
self.abort_in_progress_detection();
|
||||
}
|
||||
@@ -460,7 +578,7 @@ impl BlocklistAIInputModel {
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) {
|
||||
self.temporarily_disable_autodetection();
|
||||
self.set_input_config_internal(new_config, ctx);
|
||||
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;
|
||||
}
|
||||
@@ -488,9 +606,17 @@ impl BlocklistAIInputModel {
|
||||
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.agent_view_controller.as_ref(app).is_fullscreen() {
|
||||
if self.is_conversation_fullscreen(app) {
|
||||
ai_settings.is_ai_autodetection_enabled(app)
|
||||
} else {
|
||||
ai_settings.is_nld_in_terminal_enabled(app)
|
||||
@@ -514,6 +640,7 @@ impl BlocklistAIInputModel {
|
||||
input_type,
|
||||
is_locked: false,
|
||||
},
|
||||
None,
|
||||
ctx,
|
||||
);
|
||||
// The goal of this function is to allow autodetection to run, but if we
|
||||
@@ -539,16 +666,14 @@ impl BlocklistAIInputModel {
|
||||
} 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.agent_view_controller.as_ref(ctx).is_fullscreen(),
|
||||
ctx,
|
||||
)
|
||||
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, ctx,
|
||||
true, None, ctx,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -619,6 +744,7 @@ impl BlocklistAIInputModel {
|
||||
input_type: InputType::Shell,
|
||||
..self.input_config()
|
||||
},
|
||||
Some(InputTypeAutoDetectionSource::Denylist),
|
||||
ctx,
|
||||
);
|
||||
return;
|
||||
@@ -667,19 +793,24 @@ impl BlocklistAIInputModel {
|
||||
if matches!(current_input_type, InputType::AI)
|
||||
&& is_one_off_natural_language_word(first_token_str.to_lowercase().as_str())
|
||||
{
|
||||
return InputType::AI;
|
||||
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;
|
||||
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.
|
||||
// TODO(vorporeal): decide if we still want to do this with NldImprovements.
|
||||
if let Some(history_entries) = history_entries {
|
||||
if has_any_close_matches(
|
||||
&buffer_cloned,
|
||||
@@ -688,7 +819,7 @@ impl BlocklistAIInputModel {
|
||||
)
|
||||
.await
|
||||
{
|
||||
return InputType::Shell;
|
||||
return (InputType::Shell, InputTypeAutoDetectionSource::HistoryMatch);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -705,14 +836,13 @@ impl BlocklistAIInputModel {
|
||||
current_input_type,
|
||||
is_agent_follow_up,
|
||||
};
|
||||
let new_input_type =
|
||||
let classification =
|
||||
classifier.detect_input_type(input.clone(), &context).await;
|
||||
|
||||
futures_lite::future::yield_now().await;
|
||||
|
||||
new_input_type
|
||||
(classification.input_type, classification.source.into())
|
||||
},
|
||||
move |me, new_input_type, ctx| {
|
||||
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.
|
||||
@@ -730,6 +860,7 @@ impl BlocklistAIInputModel {
|
||||
input_type: new_input_type,
|
||||
..me.input_config()
|
||||
},
|
||||
Some(decision_source),
|
||||
ctx,
|
||||
);
|
||||
if current_input_type != new_input_type {
|
||||
|
||||
Reference in New Issue
Block a user