diff --git a/app/src/ai/blocklist/agent_view/agent_input_footer/mod.rs b/app/src/ai/blocklist/agent_view/agent_input_footer/mod.rs index 6d15eed1f4..d5e4eb113f 100644 --- a/app/src/ai/blocklist/agent_view/agent_input_footer/mod.rs +++ b/app/src/ai/blocklist/agent_view/agent_input_footer/mod.rs @@ -50,6 +50,7 @@ pub(crate) use self::environment_selector::sort_environments_by_recency; pub(crate) use self::environment_selector::{ EnvironmentSelector, EnvironmentSelectorEvent, EnvironmentSelectorTarget, }; +use crate::ai::blocklist::agent_view::is_in_cloud_context; use crate::ai::blocklist::history_model::{BlocklistAIHistoryEvent, BlocklistAIHistoryModel}; use crate::ai::blocklist::prompt::prompt_alert::{PromptAlertEvent, PromptAlertView}; use crate::ai::blocklist::usage::icon_for_context_window_usage; @@ -121,6 +122,8 @@ const DISABLE_NLD_TOOLTIP: &str = "Disable terminal command autodetection"; const FAST_FORWARD_ON_TOOLTIP: &str = "Turn off auto-approve all agent actions"; const FAST_FORWARD_OFF_TOOLTIP: &str = "Auto-approve all agent actions for this task"; +const FAST_FORWARD_LOCKED_TOOLTIP: &str = + "Fast forward is always enabled for cloud agent conversations"; const START_REMOTE_CONTROL_TOOLTIP: &str = "Start remote control"; const START_REMOTE_CONTROL_LOGIN_REQUIRED_TOOLTIP: &str = "Log in to use /remote-control"; @@ -358,6 +361,7 @@ impl AgentInputFooter { .with_tooltip(FAST_FORWARD_OFF_TOOLTIP) .with_size(button_size) .with_tooltip_alignment(TooltipAlignment::Left) + .with_disabled_theme(FastForwardLockedTheme) .on_click(|ctx| { ctx.dispatch_typed_action(TerminalAction::ToggleAutoexecuteMode); }) @@ -731,6 +735,12 @@ impl AgentInputFooter { me.update_ftu_callout_render_state(ctx); } }); + ctx.subscribe_to_model( + &display_chip_config.agent_view_controller, + |me, _, _, ctx| { + me.sync_fast_forward_button(ctx); + }, + ); // Keep the remote-control chip in sync with login state so we can // disable it and swap the tooltip when the user is anonymous or @@ -1946,26 +1956,40 @@ impl AgentInputFooter { } fn sync_fast_forward_button(&self, ctx: &mut ViewContext) { + // In cloud agent conversations fast forward is force-enabled. + let terminal_model = self.terminal_model.lock(); + let is_force_enabled = is_in_cloud_context( + terminal_model.block_list().agent_view_state(), + &terminal_model, + ); + drop(terminal_model); + // Read directly from the conversation, same data source as the warping // indicator footer's auto-approve chip. let is_active = BlocklistAIHistoryModel::as_ref(ctx) .active_conversation(self.terminal_view_id) .map(|c| c.autoexecute_any_action()) - .unwrap_or(false); + .unwrap_or(false) + || is_force_enabled; + let icon = if is_active { Icon::FastForwardFilled } else { Icon::FastForward }; - let tooltip = if is_active { + let tooltip = if is_force_enabled { + FAST_FORWARD_LOCKED_TOOLTIP + } else if is_active { FAST_FORWARD_ON_TOOLTIP } else { FAST_FORWARD_OFF_TOOLTIP }; + self.fast_forward_button.update(ctx, |button, ctx| { button.set_icon(Some(icon), ctx); button.set_tooltip(Some(tooltip), ctx); button.set_active(is_active, ctx); + button.set_disabled(is_force_enabled, ctx); }); } @@ -2839,6 +2863,38 @@ impl ActionButtonTheme for FastForwardButtonTheme { } } +/// Disabled-state theme used by the fast-forward chip when fast-forward is +/// locked on (cloud agent conversations). Delegates entirely to +/// `FastForwardButtonTheme`, but forces `hovered=true` on the background so +/// the chip still reads as "on" while the underlying button is disabled +/// (which gives us the arrow cursor and no-op click handler for free). +struct FastForwardLockedTheme; + +impl ActionButtonTheme for FastForwardLockedTheme { + fn background(&self, _hovered: bool, appearance: &Appearance) -> Option { + // Force the active (hovered) background so the disabled chip still + // visually looks like fast-forward is on. + FastForwardButtonTheme.background(true, appearance) + } + + fn text_color( + &self, + hovered: bool, + background: Option, + appearance: &Appearance, + ) -> ColorU { + FastForwardButtonTheme.text_color(hovered, background, appearance) + } + + fn border(&self, appearance: &Appearance) -> Option { + FastForwardButtonTheme.border(appearance) + } + + fn should_opt_out_of_contrast_adjustment(&self) -> bool { + FastForwardButtonTheme.should_opt_out_of_contrast_adjustment() + } +} + /// Same as `AgentInputButtonTheme`, except with one-off special active styling for the NLD button. struct NLDButtonTheme; diff --git a/app/src/ai/blocklist/agent_view/mod.rs b/app/src/ai/blocklist/agent_view/mod.rs index f3eb8d772b..9b4a165467 100644 --- a/app/src/ai/blocklist/agent_view/mod.rs +++ b/app/src/ai/blocklist/agent_view/mod.rs @@ -74,9 +74,7 @@ pub static ENTER_CLOUD_AGENT_VIEW_NEW_CONVERSATION_KEYSTROKE: LazyLock { pub summarization_start_time: Option, pub hide_responses_button: Option<(ButtonProps<'a>, bool)>, pub take_over_lrc_control_button: Option>, - pub auto_execute_button: Option>, + pub auto_execute_button: Option>, pub queue_next_prompt_button: Option>, pub stop_button: Option>, /// Inline `Check now` affordance displayed alongside `Last seen by agent ...` @@ -180,6 +180,18 @@ pub struct ButtonProps<'a> { pub is_active: bool, } +/// Props for the auto-approve / fast-forward button in the warping indicator. +/// +/// When `is_locked` is set, the button is rendered in its always-on state and +/// the click handler (plus the action dispatched by the keybinding) are no-ops. +/// Used for ambient agent conversations where fast-forward is always conceptually on. +pub struct AutoExecuteButtonProps<'a> { + pub button_handle: &'a MouseStateHandle, + pub keystroke: Option<&'a Keystroke>, + pub is_active: bool, + pub is_locked: bool, +} + pub struct ForceRefreshButtonProps<'a> { pub button_handle: &'a MouseStateHandle, /// The block the force-refresh should target. @@ -764,6 +776,7 @@ fn render_hide_responses_button( props.keystroke, tooltip_text.to_string(), props.is_active, + false, |ctx| { ctx.dispatch_typed_action(BlocklistAIStatusBarAction::ToggleHideResponses); }, @@ -795,6 +808,7 @@ pub fn render_switch_control_to_user_button( props.keystroke, tooltip.to_string(), props.is_active, + false, |ctx| { ctx.dispatch_typed_action(TerminalAction::SetInputModeTerminal); }, @@ -818,6 +832,7 @@ fn render_stop_button(props: ButtonProps, appearance: &Appearance) -> Box| { ctx.dispatch_typed_action(BlocklistAIStatusBarAction::Stop); }, @@ -855,14 +870,21 @@ fn render_queue_next_prompt_button( props.keystroke, tooltip_text.to_string(), props.is_active, + false, |ctx| { ctx.dispatch_typed_action(TerminalAction::ToggleQueueNextPrompt); }, ) } -fn render_auto_approve_button(props: ButtonProps, appearance: &Appearance) -> Box { - let icon = if props.is_active { +fn render_auto_approve_button( + props: AutoExecuteButtonProps, + appearance: &Appearance, +) -> Box { + // In locked mode (ambient/cloud agent conversations), the button is always + // rendered in its "on" state regardless of the underlying conversation state. + let is_active = props.is_active || props.is_locked; + let icon = if is_active { Icon::FastForwardFilled } else { Icon::FastForward @@ -879,7 +901,9 @@ fn render_auto_approve_button(props: ButtonProps, appearance: &Appearance) -> Bo ) .finish(); - let tooltip_text = if props.is_active { + let tooltip_text = if props.is_locked { + "Fast forward is always enabled for cloud agent conversations" + } else if is_active { "Turn off auto-approve all agent actions" } else { "Auto-approve all agent actions for this task" @@ -891,8 +915,12 @@ fn render_auto_approve_button(props: ButtonProps, appearance: &Appearance) -> Bo icon, props.keystroke, tooltip_text.to_string(), - props.is_active, - |ctx| { + is_active, + props.is_locked, + move |ctx| { + if props.is_locked { + return; + } ctx.dispatch_typed_action(TerminalAction::ToggleAutoexecuteMode); }, ) @@ -970,6 +998,7 @@ fn render_force_refresh_inline( .finish() } +#[allow(clippy::too_many_arguments)] fn render_warping_indicator_button( mouse_state: MouseStateHandle, appearance: &Appearance, @@ -977,6 +1006,7 @@ fn render_warping_indicator_button( keybinding: Option<&Keystroke>, tooltip: String, is_active: bool, + is_disabled: bool, mut on_click: F, ) -> Box where @@ -1023,6 +1053,12 @@ where UiComponentStyles::default().set_background(internal_colors::fg_overlay_3(theme).into()), ); + let cursor = if is_disabled { + Cursor::Arrow + } else { + Cursor::PointingHand + }; + let mut button = Button::new( mouse_state, styles, @@ -1032,7 +1068,7 @@ where ) .with_custom_label(button_content) .with_tooltip(move || ui_builder.tool_tip(tooltip.clone()).build().finish()) - .with_cursor(Some(Cursor::PointingHand)); + .with_cursor(Some(cursor)); if is_active { button = button.active(); diff --git a/app/src/terminal/view.rs b/app/src/terminal/view.rs index 442a56e54e..92f7868b48 100644 --- a/app/src/terminal/view.rs +++ b/app/src/terminal/view.rs @@ -234,11 +234,12 @@ use crate::ai::ambient_agents::{ use crate::ai::blocklist::agent_view::agent_input_footer::toolbar_item::AgentToolbarItemKind; use crate::ai::blocklist::agent_view::{ agent_view_bg_fill, fork_from_last_known_good_state_exchange_id, - get_agent_view_entry_block_position_id, AgentViewController, AgentViewControllerEvent, - AgentViewDisplayMode, AgentViewEntryBlockParams, AgentViewEntryOrigin, - AgentViewHeaderDisabledTheme, AgentViewHeaderTheme, AgentViewZeroStateBlock, - AgentViewZeroStateEvent, EphemeralMessageModel, ExitConfirmationTrigger, InlineAgentViewHeader, - OrchestrationPillBar, ENTER_OR_EXIT_CONFIRMATION_WINDOW, + get_agent_view_entry_block_position_id, is_in_cloud_context, AgentViewController, + AgentViewControllerEvent, AgentViewDisplayMode, AgentViewEntryBlockParams, + AgentViewEntryOrigin, AgentViewHeaderDisabledTheme, AgentViewHeaderTheme, + AgentViewZeroStateBlock, AgentViewZeroStateEvent, EphemeralMessageModel, + ExitConfirmationTrigger, InlineAgentViewHeader, OrchestrationPillBar, + ENTER_OR_EXIT_CONFIRMATION_WINDOW, }; use crate::ai::blocklist::block::cli::{CLISubagentView, CLISubagentViewEvent}; use crate::ai::blocklist::block::cli_controller::{ @@ -26335,6 +26336,19 @@ impl TypedActionView for TerminalView { self.write_codebase_index(ctx); } ToggleAutoexecuteMode => { + // Cloud (ambient) agent conversations run with fast-forward conceptually + // always on, so toggling it from the chip or keybinding is a no-op there. + let is_locked = { + let terminal_model = self.model.lock(); + is_in_cloud_context( + terminal_model.block_list().agent_view_state(), + &terminal_model, + ) + }; + if is_locked { + return; + } + // If there's a pending (blocked) requested code diff, accept it first. if let Some(ai_block) = self.last_ai_block() { ai_block.update(ctx, |ai_block, ctx| {