From f05eeb159b3cd48bb02d682774039aac020ce24a Mon Sep 17 00:00:00 2001 From: harryalbert Date: Tue, 26 May 2026 15:51:01 +0000 Subject: [PATCH 1/4] Lock fast-forward chip in cloud agent conversations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When viewing an ambient (cloud) agent conversation, the fast forward chip both in the warping line and in the agent view footer was effectively a no-op: it never reflected an action that mattered, but it still toggled the underlying state. This makes that meaningless toggle explicit: - The fast forward chip in both the warping line and the agent input footer is rendered in an always-on state when the pane is in a cloud-agent conversation (CloudAgent / ThirdPartyCloudAgent origin, the conversation transcript viewer, or a dummy cloud-mode pane). - The chip is not given disabled styling — it still looks active. - Clicks on the chip and the fast-forward keybinding are both no-ops in these contexts. The action handler short-circuits in TerminalView::ToggleAutoexecuteMode, and the warping-line chip itself skips dispatching the action. - The chip tooltip changes to "Fast forward is always enabled for cloud agent conversations". For the warping-line chip we also hide the keybinding label since the binding is locked too. The locked state is computed via the existing is_in_cloud_context helper, which already covers cloud-mode panes, transcript viewers, and third-party cloud agents. Co-Authored-By: Oz --- .../agent_view/agent_input_footer/mod.rs | 27 +++++++++++- app/src/ai/blocklist/block/status_bar.rs | 13 ++++-- .../ai/blocklist/block/view_impl/common.rs | 44 ++++++++++++++++--- app/src/terminal/view.rs | 24 +++++++--- 4 files changed, 90 insertions(+), 18 deletions(-) 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..cf1961b9d5 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"; @@ -731,6 +734,14 @@ impl AgentInputFooter { me.update_ftu_callout_render_state(ctx); } }); + // Re-sync the fast-forward chip when agent view is entered/exited so the + // locked-on state for cloud agent conversations reflects the current pane. + 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,18 +1957,30 @@ impl AgentInputFooter { } fn sync_fast_forward_button(&self, ctx: &mut ViewContext) { + // In ambient/cloud agent conversations, the chip is locked in its always-on + // state. Clicking it (and the keybinding) are no-ops; the action handler in + // `TerminalView::handle_action` short-circuits there. + let terminal_model = self.terminal_model.lock(); + let is_locked = 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) + let conversation_is_active = BlocklistAIHistoryModel::as_ref(ctx) .active_conversation(self.terminal_view_id) .map(|c| c.autoexecute_any_action()) .unwrap_or(false); + let is_active = conversation_is_active || is_locked; let icon = if is_active { Icon::FastForwardFilled } else { Icon::FastForward }; - let tooltip = if is_active { + let tooltip = if is_locked { + FAST_FORWARD_LOCKED_TOOLTIP + } else if is_active { FAST_FORWARD_ON_TOOLTIP } else { FAST_FORWARD_OFF_TOOLTIP diff --git a/app/src/ai/blocklist/block/status_bar.rs b/app/src/ai/blocklist/block/status_bar.rs index 3e3205eedb..c9f411ed67 100644 --- a/app/src/ai/blocklist/block/status_bar.rs +++ b/app/src/ai/blocklist/block/status_bar.rs @@ -25,8 +25,8 @@ use super::cli_controller::{CLISubagentController, CLISubagentEvent, UserTakeOve use super::model::{AIBlockModel, AIBlockModelImpl, AIBlockOutputStatus}; use super::view_impl::common::{ render_switch_control_to_user_button, render_warping_indicator, render_warping_indicator_base, - ButtonProps, ForceRefreshButtonProps, MaybeShimmeringText, WarpingIndicatorProps, WarpingProps, - LOAD_OUTPUT_MESSAGE, WAITING_FOR_USER_INPUT_MESSAGE, + AutoExecuteButtonProps, ButtonProps, ForceRefreshButtonProps, MaybeShimmeringText, + WarpingIndicatorProps, WarpingProps, LOAD_OUTPUT_MESSAGE, WAITING_FOR_USER_INPUT_MESSAGE, }; use crate::ai::agent::conversation::AIConversationId; use crate::ai::agent::{ @@ -37,7 +37,8 @@ use crate::ai::agent_tips::AITipModel; use crate::ai::blocklist::agent_view::child_agent_status_card::ChildAgentStatusCard; use crate::ai::blocklist::agent_view::shortcuts::AgentShortcutViewModel; use crate::ai::blocklist::agent_view::{ - agent_view_bg_fill, AgentMessageBar, AgentViewController, EphemeralMessageModel, + agent_view_bg_fill, is_in_cloud_context, AgentMessageBar, AgentViewController, + EphemeralMessageModel, }; use crate::ai::blocklist::model::AIBlockModelHelper; use crate::ai::blocklist::summarization_cancel_dialog::{ @@ -823,13 +824,17 @@ impl BlocklistAIStatusBar { shimmering_text_handle: &self.shimmering_text_handle, summarization_start_time: self.summarization_start_time, auto_execute_button: (!model.request_type(app).is_passive_code_diff()).then_some( - ButtonProps { + AutoExecuteButtonProps { button_handle: &self.state_handles.autoexecute_button, keystroke: self.autoexecute_keystroke.as_ref(), is_active: model .conversation(app) .map(|c| c.autoexecute_any_action()) .unwrap_or(false), + is_locked: is_in_cloud_context( + terminal_model.block_list().agent_view_state(), + &terminal_model, + ), }, ), queue_next_prompt_button: FeatureFlag::QueueSlashCommand.is_enabled().then_some( diff --git a/app/src/ai/blocklist/block/view_impl/common.rs b/app/src/ai/blocklist/block/view_impl/common.rs index f5447cf451..314ead4b52 100644 --- a/app/src/ai/blocklist/block/view_impl/common.rs +++ b/app/src/ai/blocklist/block/view_impl/common.rs @@ -159,7 +159,7 @@ pub struct WarpingProps<'a, V> { 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 associated 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. @@ -861,8 +873,14 @@ fn render_queue_next_prompt_button( ) } -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,20 +897,32 @@ 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" }; + // Hide the keybinding label when locked since the keybinding is a no-op. + let keystroke = if props.is_locked { + None + } else { + props.keystroke + }; + render_warping_indicator_button( props.button_handle.clone(), appearance, icon, - props.keystroke, + keystroke, tooltip_text.to_string(), - props.is_active, - |ctx| { + is_active, + move |ctx| { + if props.is_locked { + return; + } ctx.dispatch_typed_action(TerminalAction::ToggleAutoexecuteMode); }, ) 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| { From cdbc840cf19d6cad95bedb0d0a979a333235cd8d Mon Sep 17 00:00:00 2001 From: harryalbert Date: Tue, 26 May 2026 16:29:23 -0400 Subject: [PATCH 2/4] add fast forward for footer chip --- .../agent_view/agent_input_footer/mod.rs | 53 +++++++++++++++---- .../ai/blocklist/block/view_impl/common.rs | 27 ++++++---- 2 files changed, 59 insertions(+), 21 deletions(-) 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 cf1961b9d5..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 @@ -361,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); }) @@ -734,8 +735,6 @@ impl AgentInputFooter { me.update_ftu_callout_render_state(ctx); } }); - // Re-sync the fast-forward chip when agent view is entered/exited so the - // locked-on state for cloud agent conversations reflects the current pane. ctx.subscribe_to_model( &display_chip_config.agent_view_controller, |me, _, _, ctx| { @@ -1957,38 +1956,40 @@ impl AgentInputFooter { } fn sync_fast_forward_button(&self, ctx: &mut ViewContext) { - // In ambient/cloud agent conversations, the chip is locked in its always-on - // state. Clicking it (and the keybinding) are no-ops; the action handler in - // `TerminalView::handle_action` short-circuits there. + // In cloud agent conversations fast forward is force-enabled. let terminal_model = self.terminal_model.lock(); - let is_locked = is_in_cloud_context( + 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 conversation_is_active = BlocklistAIHistoryModel::as_ref(ctx) + let is_active = BlocklistAIHistoryModel::as_ref(ctx) .active_conversation(self.terminal_view_id) .map(|c| c.autoexecute_any_action()) - .unwrap_or(false); - let is_active = conversation_is_active || is_locked; + .unwrap_or(false) + || is_force_enabled; + let icon = if is_active { Icon::FastForwardFilled } else { Icon::FastForward }; - let tooltip = if is_locked { + 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); }); } @@ -2862,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/block/view_impl/common.rs b/app/src/ai/blocklist/block/view_impl/common.rs index 314ead4b52..3febeb4630 100644 --- a/app/src/ai/blocklist/block/view_impl/common.rs +++ b/app/src/ai/blocklist/block/view_impl/common.rs @@ -183,8 +183,8 @@ pub struct ButtonProps<'a> { /// 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 associated keybinding) are no-ops. Used for -/// ambient agent conversations where fast-forward is always conceptually on. +/// 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>, @@ -776,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); }, @@ -807,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); }, @@ -830,6 +832,7 @@ fn render_stop_button(props: ButtonProps, appearance: &Appearance) -> Box| { ctx.dispatch_typed_action(BlocklistAIStatusBarAction::Stop); }, @@ -867,6 +870,7 @@ fn render_queue_next_prompt_button( props.keystroke, tooltip_text.to_string(), props.is_active, + false, |ctx| { ctx.dispatch_typed_action(TerminalAction::ToggleQueueNextPrompt); }, @@ -905,20 +909,14 @@ fn render_auto_approve_button( "Auto-approve all agent actions for this task" }; - // Hide the keybinding label when locked since the keybinding is a no-op. - let keystroke = if props.is_locked { - None - } else { - props.keystroke - }; - render_warping_indicator_button( props.button_handle.clone(), appearance, icon, - keystroke, + props.keystroke, tooltip_text.to_string(), is_active, + props.is_locked, move |ctx| { if props.is_locked { return; @@ -1007,6 +1005,7 @@ fn render_warping_indicator_button( keybinding: Option<&Keystroke>, tooltip: String, is_active: bool, + is_disabled: bool, mut on_click: F, ) -> Box where @@ -1053,6 +1052,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, @@ -1062,7 +1067,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(); From 07d85c0b45a19f0c20e3a027817bf7dd41fab7cb Mon Sep 17 00:00:00 2001 From: harryalbert Date: Tue, 26 May 2026 16:30:26 -0400 Subject: [PATCH 3/4] broaden comment --- app/src/ai/blocklist/agent_view/mod.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) 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 Date: Wed, 27 May 2026 13:31:21 -0400 Subject: [PATCH 4/4] fix lint --- app/src/ai/blocklist/block/view_impl/common.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/ai/blocklist/block/view_impl/common.rs b/app/src/ai/blocklist/block/view_impl/common.rs index 3febeb4630..d028089111 100644 --- a/app/src/ai/blocklist/block/view_impl/common.rs +++ b/app/src/ai/blocklist/block/view_impl/common.rs @@ -998,6 +998,7 @@ fn render_force_refresh_inline( .finish() } +#[allow(clippy::too_many_arguments)] fn render_warping_indicator_button( mouse_state: MouseStateHandle, appearance: &Appearance,