Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
201 changes: 201 additions & 0 deletions app/src/ai/blocklist/agent_view/gui_input_mode_policy.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
//! GUI implementation of [`InputModePolicy`].

use warp_core::features::FeatureFlag;
use warpui::{AppContext, EntityId, ModelHandle, SingletonEntity};

use super::super::conversation_selection::ConversationSelectionEvent;
use super::super::input_mode_policy::{InputModePolicy, PolicyConfigUpdate};
use super::super::input_model::{InputConfig, InputType, InputTypeAutoDetectionSource};
use super::super::{BlocklistAIContextModel, ConversationSelectionHandle};
use super::AgentViewEntryOrigin;
use crate::settings::{AISettings, AISettingsChangedEvent};
use crate::terminal::cli_agent_sessions::CLIAgentSessionsModel;

/// GUI input-mode policy. The surface is either a fullscreen agent view or a
/// top-level terminal (when `FeatureFlag::AgentView` is enabled), each with its
/// own autodetection setting, and AI input may only be locked inside an agent
/// view or an open CLI-agent rich input session.
pub(crate) struct GuiInputModePolicy {
conversation_selection: ConversationSelectionHandle,
ai_context_model: ModelHandle<BlocklistAIContextModel>,
terminal_surface_id: EntityId,
}

impl GuiInputModePolicy {
/// Creates the GUI policy for a terminal surface.
pub(crate) fn new(
conversation_selection: ConversationSelectionHandle,
ai_context_model: ModelHandle<BlocklistAIContextModel>,
terminal_surface_id: EntityId,
) -> Self {
Self {
conversation_selection,
ai_context_model,
terminal_surface_id,
}
}
}

impl InputModePolicy for GuiInputModePolicy {
fn initial_config(&self, app: &AppContext) -> InputConfig {
let is_autodetection_enabled = if FeatureFlag::AgentView.is_enabled() {
AISettings::as_ref(app).is_nld_in_terminal_enabled(app)
} else {
AISettings::as_ref(app).is_ai_autodetection_enabled(app)
};
InputConfig {
input_type: InputType::Shell,
is_locked: !is_autodetection_enabled,
}
}

fn allows_locked_ai_input(&self, app: &AppContext) -> 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).
!FeatureFlag::AgentView.is_enabled()
|| self
.conversation_selection
.as_ref(app)
.is_conversation_active(app)
|| CLIAgentSessionsModel::as_ref(app).is_input_open(self.terminal_surface_id)
}

fn is_autodetection_enabled(&self, app: &AppContext) -> bool {
let ai_settings = AISettings::as_ref(app);
if FeatureFlag::AgentView.is_enabled() {
if self
.conversation_selection
.as_ref(app)
.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)
}
}

fn config_on_conversation_selection_changed(
&self,
event: &ConversationSelectionEvent,
current: InputConfig,
app: &AppContext,
) -> Option<PolicyConfigUpdate> {
match event {
ConversationSelectionEvent::Changed => None,
ConversationSelectionEvent::Activated {
is_fullscreen,
origin,
} => {
if !is_fullscreen {
Some(PolicyConfigUpdate::with_source(
InputConfig {
input_type: InputType::AI,
is_locked: true,
},
InputTypeAutoDetectionSource::InlineAgentViewEntry,
))
} else if matches!(origin, AgentViewEntryOrigin::ClearBuffer) {
let is_autodetection_enabled =
AISettings::as_ref(app).is_ai_autodetection_enabled(app);
Some(PolicyConfigUpdate::new(InputConfig {
input_type: current.input_type,
is_locked: !is_autodetection_enabled,
}))
} else if self.ai_context_model.as_ref(app).has_locking_attachment() {
Some(PolicyConfigUpdate::with_source(
InputConfig {
input_type: InputType::AI,
is_locked: true,
},
InputTypeAutoDetectionSource::AttachmentForcedAi,
))
} else {
let is_autodetection_enabled =
AISettings::as_ref(app).is_ai_autodetection_enabled(app);
Some(PolicyConfigUpdate {
config: InputConfig {
input_type: InputType::AI,
is_locked: !is_autodetection_enabled,
},
decision_source: None,
temporarily_disable_autodetection: is_autodetection_enabled,
})
}
}
ConversationSelectionEvent::Deactivated {
is_exit_before_new_entrance,
..
} => {
if *is_exit_before_new_entrance {
return None;
}
let is_nld_in_terminal_enabled =
AISettings::as_ref(app).is_nld_in_terminal_enabled(app);
Some(PolicyConfigUpdate::new(InputConfig {
input_type: InputType::Shell,
is_locked: !is_nld_in_terminal_enabled,
}))
}
}
}

fn config_on_ai_settings_changed(
&self,
event: &AISettingsChangedEvent,
current: InputConfig,
is_autodetection_enabled_for_current_context: bool,
app: &AppContext,
) -> Option<PolicyConfigUpdate> {
match event {
AISettingsChangedEvent::AIAutoDetectionEnabled { .. }
if FeatureFlag::AgentView.is_enabled() =>
{
if self
.conversation_selection
.as_ref(app)
.is_conversation_fullscreen(app)
{
// Use context-specific check to determine if autodetection should be enabled
let is_nld_enabled = AISettings::as_ref(app).is_ai_autodetection_enabled(app);

// If autodetection is enabled, unlock the input.
Some(PolicyConfigUpdate::new(InputConfig {
is_locked: !is_nld_enabled,
input_type: InputType::AI,
}))
} else {
None
}
}
AISettingsChangedEvent::AIAutoDetectionEnabled { .. } => {
// If autodetection is enabled, unlock the input.
Some(PolicyConfigUpdate::new(InputConfig {
is_locked: !is_autodetection_enabled_for_current_context,
..current
}))
}
AISettingsChangedEvent::NLDInTerminalEnabled { .. }
if FeatureFlag::AgentView.is_enabled()
&& !self
.conversation_selection
.as_ref(app)
.is_conversation_active(app) =>
{
let is_nld_enabled = AISettings::as_ref(app).is_nld_in_terminal_enabled(app);
Some(PolicyConfigUpdate::new(InputConfig {
is_locked: !is_nld_enabled,
input_type: InputType::Shell,
}))
}
_ => None,
}
}
}
2 changes: 2 additions & 0 deletions app/src/ai/blocklist/agent_view/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ mod agent_view_block;
mod controller;
mod conversation_selection;
mod ephemeral_message_model;
mod gui_input_mode_policy;
mod inline_agent_view_header;
// TODO: Move orchestration_conversation_links module import elsewhere.
pub(crate) mod orchestration_avatar;
Expand All @@ -21,6 +22,7 @@ pub use agent_view_block::*;
pub use controller::*;
pub(crate) use conversation_selection::AgentViewConversationSelection;
pub use ephemeral_message_model::*;
pub(crate) use gui_input_mode_policy::GuiInputModePolicy;
pub use inline_agent_view_header::*;
pub use orchestration_pill_bar::{render_orchestration_breadcrumbs, OrchestrationPillBar};
use pathfinder_color::ColorU;
Expand Down
106 changes: 106 additions & 0 deletions app/src/ai/blocklist/input_mode_policy.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
//! View-supplied policy for input-mode decisions.
//!
//! [`BlocklistAIInputModel`](super::BlocklistAIInputModel) is shared between
//! frontends (the GUI terminal input and the TUI prompt input), but several of
//! its decisions depend on view concepts the model cannot know about — e.g.
//! whether the surface distinguishes "fullscreen agent view" from "top-level
//! terminal", or whether locking the input to AI is allowed outside a
//! conversation. Each frontend supplies those answers via [`InputModePolicy`],
//! mirroring how [`ConversationSelection`](super::conversation_selection::ConversationSelection)
//! injects per-view selection semantics.
//!
//! The GUI implementation lives in
//! `super::agent_view::GuiInputModePolicy`; the TUI implementation lives in
//! `crates/warp_tui/src/input_mode_policy.rs`.

use std::rc::Rc;

use warpui::AppContext;

use super::conversation_selection::ConversationSelectionEvent;
use super::input_model::{InputConfig, InputTypeAutoDetectionSource};
use crate::settings::AISettingsChangedEvent;

/// A config write produced by an [`InputModePolicy`] decision. The fields
/// mirror exactly what the previously-inlined GUI decision code passed to the
/// model's internal setter: the config, the decision source recorded with it,
/// and (for one agent-view entry path) a brief autodetection suppression so
/// the applied config isn't immediately overridden by a keystroke-driven
/// detection pass.
pub struct PolicyConfigUpdate {
/// The config to apply.
pub config: InputConfig,
/// The decision source recorded alongside the config.
pub decision_source: Option<InputTypeAutoDetectionSource>,
/// Whether to briefly suppress autodetection before applying.
pub temporarily_disable_autodetection: bool,
}

impl PolicyConfigUpdate {
/// An update with no decision source and no autodetection suppression.
pub fn new(config: InputConfig) -> Self {
Self {
config,
decision_source: None,
temporarily_disable_autodetection: false,
}
}

/// An update recorded with `decision_source`, without autodetection
/// suppression.
pub fn with_source(config: InputConfig, decision_source: InputTypeAutoDetectionSource) -> Self {
Self {
config,
decision_source: Some(decision_source),
temporarily_disable_autodetection: false,
}
}
}

/// Per-view policy consulted by [`BlocklistAIInputModel`](super::BlocklistAIInputModel)
/// for decisions it cannot make view-agnostically: lock gating, the
/// autodetection setting for the surface's current context, and reactive
/// config transitions driven by conversation-selection and settings events.
///
/// The reactive hooks receive the raw event and decide the config to apply,
/// so view-specific event payloads (fullscreen vs. inline, entry origins)
/// stay a concern of the implementing view.
pub trait InputModePolicy: 'static {
/// The config the surface starts with.
fn initial_config(&self, app: &AppContext) -> InputConfig;

/// Whether the input may currently be locked to AI. When this returns
/// `false`, `{AI, locked}` config writes are rejected.
fn allows_locked_ai_input(&self, app: &AppContext) -> bool;

/// Whether NL autodetection is enabled for the surface's current context.
/// This is the raw setting lookup; the model layers its own view-agnostic
/// guards (agent-in-control, pending attachments) on top.
fn is_autodetection_enabled(&self, app: &AppContext) -> bool;

/// The config to apply in response to a conversation-selection event, or
/// `None` to leave the config unchanged.
fn config_on_conversation_selection_changed(
&self,
event: &ConversationSelectionEvent,
current: InputConfig,
app: &AppContext,
) -> Option<PolicyConfigUpdate>;

/// The config to apply when AI settings change, or `None` to leave the
/// config unchanged. `is_autodetection_enabled_for_current_context` is the
/// model's guarded autodetection state (agent-in-control and attachment
/// checks layered over [`Self::is_autodetection_enabled`]). Computing it
/// takes the terminal-model lock, so the model only computes it for
/// `AIAutoDetectionEnabled` events; for all other events it is `false`.
fn config_on_ai_settings_changed(
&self,
event: &AISettingsChangedEvent,
current: InputConfig,
is_autodetection_enabled_for_current_context: bool,
app: &AppContext,
) -> Option<PolicyConfigUpdate>;
}

/// Shared handle to a view-supplied [`InputModePolicy`].
pub type InputModePolicyHandle = Rc<dyn InputModePolicy>;
Loading
Loading