diff --git a/assets/icons/zed_burn_mode.svg b/assets/icons/zed_burn_mode.svg new file mode 100644 index 0000000000..544368d8e0 --- /dev/null +++ b/assets/icons/zed_burn_mode.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/zed_burn_mode_on.svg b/assets/icons/zed_burn_mode_on.svg new file mode 100644 index 0000000000..94230b6fd6 --- /dev/null +++ b/assets/icons/zed_burn_mode_on.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/assets/icons/zed_max_mode.svg b/assets/icons/zed_max_mode.svg deleted file mode 100644 index 969785a83f..0000000000 --- a/assets/icons/zed_max_mode.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index eab1f72ff1..243406277e 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -248,7 +248,9 @@ "ctrl-shift-i": "agent::ToggleOptionsMenu", "shift-alt-escape": "agent::ExpandMessageEditor", "ctrl-alt-e": "agent::RemoveAllContext", - "ctrl-shift-e": "project_panel::ToggleFocus" + "ctrl-shift-e": "project_panel::ToggleFocus", + "ctrl-shift-enter": "agent::ContinueThread", + "alt-enter": "agent::ContinueWithBurnMode" } }, { diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 570be05a31..5afb6e97c4 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -283,7 +283,9 @@ "cmd-shift-i": "agent::ToggleOptionsMenu", "shift-alt-escape": "agent::ExpandMessageEditor", "cmd-alt-e": "agent::RemoveAllContext", - "cmd-shift-e": "project_panel::ToggleFocus" + "cmd-shift-e": "project_panel::ToggleFocus", + "cmd-shift-enter": "agent::ContinueThread", + "alt-enter": "agent::ContinueWithBurnMode" } }, { diff --git a/crates/agent/src/active_thread.rs b/crates/agent/src/active_thread.rs index 6dbbd2b69f..46f924c153 100644 --- a/crates/agent/src/active_thread.rs +++ b/crates/agent/src/active_thread.rs @@ -1778,6 +1778,11 @@ impl ActiveThread { let Some(message) = self.thread.read(cx).message(message_id) else { return Empty.into_any(); }; + + if message.is_hidden { + return Empty.into_any(); + } + let message_creases = message.creases.clone(); let Some(rendered_message) = self.rendered_messages_by_id.get(&message_id) else { diff --git a/crates/agent/src/agent.rs b/crates/agent/src/agent.rs index b4d1abdea4..f5ae6097a5 100644 --- a/crates/agent/src/agent.rs +++ b/crates/agent/src/agent.rs @@ -87,6 +87,8 @@ actions!( Follow, ResetTrialUpsell, ResetTrialEndUpsell, + ContinueThread, + ContinueWithBurnMode, ] ); diff --git a/crates/agent/src/agent_panel.rs b/crates/agent/src/agent_panel.rs index 0d59ad9595..324f98c2fd 100644 --- a/crates/agent/src/agent_panel.rs +++ b/crates/agent/src/agent_panel.rs @@ -7,7 +7,7 @@ use std::time::Duration; use db::kvp::{Dismissable, KEY_VALUE_STORE}; use serde::{Deserialize, Serialize}; -use agent_settings::{AgentDockPosition, AgentSettings, DefaultView}; +use agent_settings::{AgentDockPosition, AgentSettings, CompletionMode, DefaultView}; use anyhow::{Result, anyhow}; use assistant_context_editor::{ AgentPanelDelegate, AssistantContext, ConfigurationError, ContextEditor, ContextEvent, @@ -41,8 +41,8 @@ use theme::ThemeSettings; use time::UtcOffset; use ui::utils::WithRemSize; use ui::{ - Banner, CheckboxWithLabel, ContextMenu, KeyBinding, PopoverMenu, PopoverMenuHandle, - ProgressBar, Tab, Tooltip, Vector, VectorName, prelude::*, + Banner, CheckboxWithLabel, ContextMenu, ElevationIndex, KeyBinding, PopoverMenu, + PopoverMenuHandle, ProgressBar, Tab, Tooltip, Vector, VectorName, prelude::*, }; use util::{ResultExt as _, maybe}; use workspace::dock::{DockPosition, Panel, PanelEvent}; @@ -64,10 +64,11 @@ use crate::thread_history::{HistoryEntryElement, ThreadHistory}; use crate::thread_store::ThreadStore; use crate::ui::AgentOnboardingModal; use crate::{ - AddContextServer, AgentDiffPane, ContextStore, DeleteRecentlyOpenThread, ExpandMessageEditor, - Follow, InlineAssistant, NewTextThread, NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff, - OpenHistory, ResetTrialEndUpsell, ResetTrialUpsell, TextThreadStore, ThreadEvent, - ToggleContextPicker, ToggleNavigationMenu, ToggleOptionsMenu, + AddContextServer, AgentDiffPane, ContextStore, ContinueThread, ContinueWithBurnMode, + DeleteRecentlyOpenThread, ExpandMessageEditor, Follow, InlineAssistant, NewTextThread, + NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff, OpenHistory, ResetTrialEndUpsell, + ResetTrialUpsell, TextThreadStore, ThreadEvent, ToggleContextPicker, ToggleNavigationMenu, + ToggleOptionsMenu, }; const AGENT_PANEL_KEY: &str = "agent_panel"; @@ -1283,6 +1284,26 @@ impl AgentPanel { matches!(self.active_view, ActiveView::Thread { .. }) } + fn continue_conversation(&mut self, window: &mut Window, cx: &mut Context) { + let thread_state = self.thread.read(cx).thread().read(cx); + if !thread_state.tool_use_limit_reached() { + return; + } + + let model = thread_state.configured_model().map(|cm| cm.model.clone()); + if let Some(model) = model { + self.thread.update(cx, |active_thread, cx| { + active_thread.thread().update(cx, |thread, cx| { + thread.insert_invisible_continue_message(cx); + thread.advance_prompt_id(); + thread.send_to_model(model, Some(window.window_handle()), cx); + }); + }); + } else { + log::warn!("No configured model available for continuation"); + } + } + pub(crate) fn active_context_editor(&self) -> Option> { match &self.active_view { ActiveView::PromptEditor { context_editor, .. } => Some(context_editor.clone()), @@ -2574,7 +2595,11 @@ impl AgentPanel { }) } - fn render_tool_use_limit_reached(&self, cx: &mut Context) -> Option { + fn render_tool_use_limit_reached( + &self, + window: &mut Window, + cx: &mut Context, + ) -> Option { let tool_use_limit_reached = self .thread .read(cx) @@ -2593,17 +2618,59 @@ impl AgentPanel { .configured_model()? .model; - let max_mode_upsell = if model.supports_max_mode() { - " Enable max mode for unlimited tool use." - } else { - "" - }; + let focus_handle = self.focus_handle(cx); let banner = Banner::new() .severity(ui::Severity::Info) - .child(h_flex().child(Label::new(format!( - "Consecutive tool use limit reached.{max_mode_upsell}" - )))); + .child(Label::new("Consecutive tool use limit reached.").size(LabelSize::Small)) + .action_slot( + h_flex() + .gap_1() + .child( + Button::new("continue-conversation", "Continue") + .layer(ElevationIndex::ModalSurface) + .label_size(LabelSize::Small) + .key_binding( + KeyBinding::for_action_in( + &ContinueThread, + &focus_handle, + window, + cx, + ) + .map(|kb| kb.size(rems_from_px(10.))), + ) + .on_click(cx.listener(|this, _, window, cx| { + this.continue_conversation(window, cx); + })), + ) + .when(model.supports_max_mode(), |this| { + this.child( + Button::new("continue-burn-mode", "Continue with Burn Mode") + .style(ButtonStyle::Filled) + .style(ButtonStyle::Tinted(ui::TintColor::Accent)) + .layer(ElevationIndex::ModalSurface) + .label_size(LabelSize::Small) + .key_binding( + KeyBinding::for_action_in( + &ContinueWithBurnMode, + &focus_handle, + window, + cx, + ) + .map(|kb| kb.size(rems_from_px(10.))), + ) + .tooltip(Tooltip::text("Enable Burn Mode for unlimited tool use.")) + .on_click(cx.listener(|this, _, window, cx| { + this.thread.update(cx, |active_thread, cx| { + active_thread.thread().update(cx, |thread, _cx| { + thread.set_completion_mode(CompletionMode::Max); + }); + }); + this.continue_conversation(window, cx); + })), + ) + }), + ); Some(div().px_2().pb_2().child(banner).into_any_element()) } @@ -2958,9 +3025,9 @@ impl Render for AgentPanel { // non-obvious implications to the layout of children. // // If you need to change it, please confirm: - // - The message editor expands (⌘esc) correctly + // - The message editor expands (cmd-option-esc) correctly // - When expanded, the buttons at the bottom of the panel are displayed correctly - // - Font size works as expected and can be changed with ⌘+/⌘- + // - Font size works as expected and can be changed with cmd-+/cmd- // - Scrolling in all views works as expected // - Files can be dropped into the panel let content = v_flex() @@ -2987,6 +3054,17 @@ impl Render for AgentPanel { .on_action(cx.listener(Self::decrease_font_size)) .on_action(cx.listener(Self::reset_font_size)) .on_action(cx.listener(Self::toggle_zoom)) + .on_action(cx.listener(|this, _: &ContinueThread, window, cx| { + this.continue_conversation(window, cx); + })) + .on_action(cx.listener(|this, _: &ContinueWithBurnMode, window, cx| { + this.thread.update(cx, |active_thread, cx| { + active_thread.thread().update(cx, |thread, _cx| { + thread.set_completion_mode(CompletionMode::Max); + }); + }); + this.continue_conversation(window, cx); + })) .child(self.render_toolbar(window, cx)) .children(self.render_upsell(window, cx)) .children(self.render_trial_end_upsell(window, cx)) @@ -2994,7 +3072,7 @@ impl Render for AgentPanel { ActiveView::Thread { .. } => parent .relative() .child(self.render_active_thread_or_empty_state(window, cx)) - .children(self.render_tool_use_limit_reached(cx)) + .children(self.render_tool_use_limit_reached(window, cx)) .child(h_flex().child(self.message_editor.clone())) .children(self.render_last_error(cx)) .child(self.render_drag_target(cx)), diff --git a/crates/agent/src/message_editor.rs b/crates/agent/src/message_editor.rs index 3256299c89..a53fc475f4 100644 --- a/crates/agent/src/message_editor.rs +++ b/crates/agent/src/message_editor.rs @@ -480,16 +480,18 @@ impl MessageEditor { let active_completion_mode = thread.completion_mode(); let max_mode_enabled = active_completion_mode == CompletionMode::Max; + let icon = if max_mode_enabled { + IconName::ZedBurnModeOn + } else { + IconName::ZedBurnMode + }; Some( - Button::new("max-mode", "Max Mode") - .label_size(LabelSize::Small) - .color(Color::Muted) - .icon(IconName::ZedMaxMode) + IconButton::new("burn-mode", icon) .icon_size(IconSize::Small) .icon_color(Color::Muted) - .icon_position(IconPosition::Start) .toggle_state(max_mode_enabled) + .selected_icon_color(Color::Error) .on_click(cx.listener(move |this, _event, _window, cx| { this.thread.update(cx, |thread, _cx| { thread.set_completion_mode(match active_completion_mode { @@ -686,7 +688,6 @@ impl MessageEditor { .justify_between() .child( h_flex() - .gap_1() .child(self.render_follow_toggle(cx)) .children(self.render_max_mode_toggle(cx)), ) diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index d0b63e0157..78a0f855ef 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -115,6 +115,7 @@ pub struct Message { pub segments: Vec, pub loaded_context: LoadedContext, pub creases: Vec, + pub is_hidden: bool, } impl Message { @@ -540,6 +541,7 @@ impl Thread { context: None, }) .collect(), + is_hidden: message.is_hidden, }) .collect(), next_message_id, @@ -560,7 +562,7 @@ impl Thread { cumulative_token_usage: serialized.cumulative_token_usage, exceeded_window_error: None, last_usage: None, - tool_use_limit_reached: false, + tool_use_limit_reached: serialized.tool_use_limit_reached, feedback: None, message_feedback: HashMap::default(), last_auto_capture_at: None, @@ -849,7 +851,7 @@ impl Thread { .get(ix + 1) .and_then(|message| { self.message(message.id) - .map(|next_message| next_message.role == Role::User) + .map(|next_message| next_message.role == Role::User && !next_message.is_hidden) }) .unwrap_or(false) } @@ -951,6 +953,7 @@ impl Thread { vec![MessageSegment::Text(text.into())], loaded_context.loaded_context, creases, + false, cx, ); @@ -966,6 +969,20 @@ impl Thread { message_id } + pub fn insert_invisible_continue_message(&mut self, cx: &mut Context) -> MessageId { + let id = self.insert_message( + Role::User, + vec![MessageSegment::Text("Continue where you left off".into())], + LoadedContext::default(), + vec![], + true, + cx, + ); + self.pending_checkpoint = None; + + id + } + pub fn insert_assistant_message( &mut self, segments: Vec, @@ -976,6 +993,7 @@ impl Thread { segments, LoadedContext::default(), Vec::new(), + false, cx, ) } @@ -986,6 +1004,7 @@ impl Thread { segments: Vec, loaded_context: LoadedContext, creases: Vec, + is_hidden: bool, cx: &mut Context, ) -> MessageId { let id = self.next_message_id.post_inc(); @@ -995,6 +1014,7 @@ impl Thread { segments, loaded_context, creases, + is_hidden, }); self.touch_updated_at(); cx.emit(ThreadEvent::MessageAdded(id)); @@ -1135,6 +1155,7 @@ impl Thread { label: crease.metadata.label.clone(), }) .collect(), + is_hidden: message.is_hidden, }) .collect(), initial_project_snapshot, @@ -1150,6 +1171,7 @@ impl Thread { model: model.model.id().0.to_string(), }), completion_mode: Some(this.completion_mode), + tool_use_limit_reached: this.tool_use_limit_reached, }) }) } @@ -1781,6 +1803,7 @@ impl Thread { thread.cancel_last_completion(window, cx); } } + cx.emit(ThreadEvent::Stopped(result.map_err(Arc::new))); if let Some((request_callback, (request, response_events))) = thread diff --git a/crates/agent/src/thread_store.rs b/crates/agent/src/thread_store.rs index 8cc29e32ab..8c6fc909e9 100644 --- a/crates/agent/src/thread_store.rs +++ b/crates/agent/src/thread_store.rs @@ -676,6 +676,8 @@ pub struct SerializedThread { pub model: Option, #[serde(default)] pub completion_mode: Option, + #[serde(default)] + pub tool_use_limit_reached: bool, } #[derive(Serialize, Deserialize, Debug)] @@ -757,6 +759,8 @@ pub struct SerializedMessage { pub context: String, #[serde(default)] pub creases: Vec, + #[serde(default)] + pub is_hidden: bool, } #[derive(Debug, Serialize, Deserialize)] @@ -815,6 +819,7 @@ impl LegacySerializedThread { exceeded_window_error: None, model: None, completion_mode: None, + tool_use_limit_reached: false, } } } @@ -840,6 +845,7 @@ impl LegacySerializedMessage { tool_results: self.tool_results, context: String::new(), creases: Vec::new(), + is_hidden: false, } } } diff --git a/crates/agent/src/ui/max_mode_tooltip.rs b/crates/agent/src/ui/max_mode_tooltip.rs index c6a5116e2e..d1bd94c201 100644 --- a/crates/agent/src/ui/max_mode_tooltip.rs +++ b/crates/agent/src/ui/max_mode_tooltip.rs @@ -18,18 +18,24 @@ impl MaxModeTooltip { impl Render for MaxModeTooltip { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let icon = if self.selected { + IconName::ZedBurnModeOn + } else { + IconName::ZedBurnMode + }; + + let title = h_flex() + .gap_1() + .child(Icon::new(icon).size(IconSize::Small)) + .child(Label::new("Burn Mode")); + tooltip_container(window, cx, |this, _, _| { - this.gap_1() + this.gap_0p5() .map(|header| if self.selected { header.child( h_flex() .justify_between() - .child( - h_flex() - .gap_1p5() - .child(Icon::new(IconName::ZedMaxMode).size(IconSize::Small).color(Color::Accent)) - .child(Label::new("Zed's Max Mode")) - ) + .child(title) .child( h_flex() .gap_0p5() @@ -38,18 +44,13 @@ impl Render for MaxModeTooltip { ) ) } else { - header.child( - h_flex() - .gap_1p5() - .child(Icon::new(IconName::ZedMaxMode).size(IconSize::Small)) - .child(Label::new("Zed's Max Mode")) - ) + header.child(title) }) .child( div() .max_w_72() .child( - Label::new("This mode enables models to use large context windows, unlimited tool calls, and other capabilities for expanded reasoning, offering an unfettered agentic experience.") + Label::new("Enables models to use large context windows, unlimited tool calls, and other capabilities for expanded reasoning, offering an unfettered agentic experience.") .size(LabelSize::Small) .color(Color::Muted) ) diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs index 3f51383f21..6d12edff83 100644 --- a/crates/icons/src/icons.rs +++ b/crates/icons/src/icons.rs @@ -256,7 +256,8 @@ pub enum IconName { XCircle, ZedAssistant, ZedAssistantFilled, - ZedMaxMode, + ZedBurnMode, + ZedBurnModeOn, ZedPredict, ZedPredictDisabled, ZedPredictDown, diff --git a/crates/ui/src/components/banner.rs b/crates/ui/src/components/banner.rs index d5bee5463f..043791cdd8 100644 --- a/crates/ui/src/components/banner.rs +++ b/crates/ui/src/components/banner.rs @@ -86,7 +86,7 @@ impl RenderOnce for Banner { IconName::Info, Color::Muted, cx.theme().status().info_background.opacity(0.5), - cx.theme().colors().border_variant, + cx.theme().colors().border.opacity(0.5), ), Severity::Success => ( IconName::Check,