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
60 changes: 58 additions & 2 deletions app/src/ai/blocklist/agent_view/agent_input_footer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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);
})
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1946,26 +1956,40 @@ impl AgentInputFooter {
}

fn sync_fast_forward_button(&self, ctx: &mut ViewContext<Self>) {
// 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);
});
}

Expand Down Expand Up @@ -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<Fill> {
// 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<Fill>,
appearance: &Appearance,
) -> ColorU {
FastForwardButtonTheme.text_color(hovered, background, appearance)
}

fn border(&self, appearance: &Appearance) -> Option<ColorU> {
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;

Expand Down
4 changes: 1 addition & 3 deletions app/src/ai/blocklist/agent_view/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,7 @@ pub static ENTER_CLOUD_AGENT_VIEW_NEW_CONVERSATION_KEYSTROKE: LazyLock<Keystroke
}
});

/// Returns `true` when the current pane is in a cloud or remote context where
/// local-to-cloud handoff is not applicable. Use this to gate the `&` hint,
/// handoff chip, `/handoff` command activation, and `&` prefix activation.
/// Returns `true` when the current pane is in a cloud or remote context.
pub fn is_in_cloud_context(
agent_view_state: &AgentViewState,
terminal_model: &TerminalModel,
Expand Down
13 changes: 9 additions & 4 deletions app/src/ai/blocklist/block/status_bar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand All @@ -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::{
Expand Down Expand Up @@ -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(
Expand Down
50 changes: 43 additions & 7 deletions app/src/ai/blocklist/block/view_impl/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ pub struct WarpingProps<'a, V> {
pub summarization_start_time: Option<instant::Instant>,
pub hide_responses_button: Option<(ButtonProps<'a>, bool)>,
pub take_over_lrc_control_button: Option<ButtonProps<'a>>,
pub auto_execute_button: Option<ButtonProps<'a>>,
pub auto_execute_button: Option<AutoExecuteButtonProps<'a>>,
pub queue_next_prompt_button: Option<ButtonProps<'a>>,
pub stop_button: Option<ButtonProps<'a>>,
/// Inline `Check now` affordance displayed alongside `Last seen by agent ...`
Expand All @@ -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.
Expand Down Expand Up @@ -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);
},
Expand Down Expand Up @@ -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);
},
Expand All @@ -818,6 +832,7 @@ fn render_stop_button(props: ButtonProps, appearance: &Appearance) -> Box<dyn El
props.keystroke,
"Stop agent task".to_string(),
props.is_active,
false,
|ctx: &mut EventContext<'_>| {
ctx.dispatch_typed_action(BlocklistAIStatusBarAction::Stop);
},
Expand Down Expand Up @@ -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<dyn Element> {
let icon = if props.is_active {
fn render_auto_approve_button(
props: AutoExecuteButtonProps,
appearance: &Appearance,
) -> Box<dyn Element> {
// 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
Expand All @@ -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"
Expand All @@ -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);
},
)
Expand Down Expand Up @@ -970,13 +998,15 @@ fn render_force_refresh_inline(
.finish()
}

#[allow(clippy::too_many_arguments)]
fn render_warping_indicator_button<F>(
mouse_state: MouseStateHandle,
appearance: &Appearance,
content: Box<dyn Element>,
keybinding: Option<&Keystroke>,
tooltip: String,
is_active: bool,
is_disabled: bool,
mut on_click: F,
) -> Box<dyn Element>
where
Expand Down Expand Up @@ -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,
Expand All @@ -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();
Expand Down
24 changes: 19 additions & 5 deletions app/src/terminal/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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| {
Expand Down
Loading