diff --git a/assets/icons/list_todo.svg b/assets/icons/list_todo.svg new file mode 100644 index 0000000000000000000000000000000000000000..1f50219418231e9d25fe9441ae1e4ae445abfcee --- /dev/null +++ b/assets/icons/list_todo.svg @@ -0,0 +1 @@ + diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 0b463266f54c3d607cd582c0b0426f8d494225d8..9012c1b0922ed99ac6e5a534f759b8a968b2f049 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -278,7 +278,9 @@ "enter": "agent::Chat", "ctrl-enter": "agent::ChatWithFollow", "ctrl-i": "agent::ToggleProfileSelector", - "shift-ctrl-r": "agent::OpenAgentDiff" + "shift-ctrl-r": "agent::OpenAgentDiff", + "ctrl-shift-y": "agent::KeepAll", + "ctrl-shift-n": "agent::RejectAll" } }, { diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 75d35f3ed3a8b16d0b13895b27db85461740dd44..05aa67f8a71f6654862eeb00c408176e98106f6c 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -315,7 +315,9 @@ "enter": "agent::Chat", "cmd-enter": "agent::ChatWithFollow", "cmd-i": "agent::ToggleProfileSelector", - "shift-ctrl-r": "agent::OpenAgentDiff" + "shift-ctrl-r": "agent::OpenAgentDiff", + "cmd-shift-y": "agent::KeepAll", + "cmd-shift-n": "agent::RejectAll" } }, { diff --git a/crates/agent/src/context_server_tool.rs b/crates/agent/src/context_server_tool.rs index 68ffefb126468b114878e0ed8857425a31fc1dbc..e4461f94de3ced9c13431de6e0eb02b7ffe646e4 100644 --- a/crates/agent/src/context_server_tool.rs +++ b/crates/agent/src/context_server_tool.rs @@ -51,6 +51,10 @@ impl Tool for ContextServerTool { true } + fn may_perform_edits(&self) -> bool { + true + } + fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result { let mut schema = self.tool.input_schema.clone(); assistant_tool::adapt_schema_to_format(&mut schema, format)?; diff --git a/crates/agent/src/message_editor.rs b/crates/agent/src/message_editor.rs index 9e3467cca6676d6ecfb3c5d542b8df5af7c8e407..484e91abfd69f55e5ea3b6e66140bc09e3e9dde2 100644 --- a/crates/agent/src/message_editor.rs +++ b/crates/agent/src/message_editor.rs @@ -6,7 +6,7 @@ use crate::agent_model_selector::{AgentModelSelector, ModelType}; use crate::context::{AgentContextKey, ContextCreasesAddon, ContextLoadResult, load_context}; use crate::tool_compatibility::{IncompatibleToolsState, IncompatibleToolsTooltip}; use crate::ui::{ - AnimatedLabel, MaxModeTooltip, + MaxModeTooltip, preview::{AgentPreview, UsageCallout}, }; use agent_settings::{AgentSettings, CompletionMode}; @@ -27,7 +27,7 @@ use gpui::{ Animation, AnimationExt, App, ClipboardEntry, Entity, EventEmitter, Focusable, Subscription, Task, TextStyle, WeakEntity, linear_color_stop, linear_gradient, point, pulsating_between, }; -use language::{Buffer, Language}; +use language::{Buffer, Language, Point}; use language_model::{ ConfiguredModel, LanguageModelRequestMessage, MessageContent, RequestUsage, ZED_CLOUD_PROVIDER_ID, @@ -51,9 +51,9 @@ use crate::profile_selector::ProfileSelector; use crate::thread::{MessageCrease, Thread, TokenUsageRatio}; use crate::thread_store::{TextThreadStore, ThreadStore}; use crate::{ - ActiveThread, AgentDiffPane, Chat, ChatWithFollow, ExpandMessageEditor, Follow, NewThread, - OpenAgentDiff, RemoveAllContext, ToggleBurnMode, ToggleContextPicker, ToggleProfileSelector, - register_agent_preview, + ActiveThread, AgentDiffPane, Chat, ChatWithFollow, ExpandMessageEditor, Follow, KeepAll, + NewThread, OpenAgentDiff, RejectAll, RemoveAllContext, ToggleBurnMode, ToggleContextPicker, + ToggleProfileSelector, register_agent_preview, }; #[derive(RegisterComponent)] @@ -459,11 +459,20 @@ impl MessageEditor { } fn handle_review_click(&mut self, window: &mut Window, cx: &mut Context) { + if self.thread.read(cx).has_pending_edit_tool_uses() { + return; + } + self.edits_expanded = true; AgentDiffPane::deploy(self.thread.clone(), self.workspace.clone(), window, cx).log_err(); cx.notify(); } + fn handle_edit_bar_expand(&mut self, cx: &mut Context) { + self.edits_expanded = !self.edits_expanded; + cx.notify(); + } + fn handle_file_click( &self, buffer: Entity, @@ -494,6 +503,40 @@ impl MessageEditor { }); } + fn handle_accept_all(&mut self, _window: &mut Window, cx: &mut Context) { + if self.thread.read(cx).has_pending_edit_tool_uses() { + return; + } + + self.thread.update(cx, |thread, cx| { + thread.keep_all_edits(cx); + }); + cx.notify(); + } + + fn handle_reject_all(&mut self, _window: &mut Window, cx: &mut Context) { + if self.thread.read(cx).has_pending_edit_tool_uses() { + return; + } + + // Since there's no reject_all_edits method in the thread API, + // we need to iterate through all buffers and reject their edits + let action_log = self.thread.read(cx).action_log().clone(); + let changed_buffers = action_log.read(cx).changed_buffers(cx); + + for (buffer, _) in changed_buffers { + self.thread.update(cx, |thread, cx| { + let buffer_snapshot = buffer.read(cx); + let start = buffer_snapshot.anchor_before(Point::new(0, 0)); + let end = buffer_snapshot.anchor_after(buffer_snapshot.max_point()); + thread + .reject_edits_in_ranges(buffer, vec![start..end], cx) + .detach(); + }); + } + cx.notify(); + } + fn render_max_mode_toggle(&self, cx: &mut Context) -> Option { let thread = self.thread.read(cx); let model = thread.configured_model(); @@ -615,6 +658,12 @@ impl MessageEditor { .on_action(cx.listener(Self::move_up)) .on_action(cx.listener(Self::expand_message_editor)) .on_action(cx.listener(Self::toggle_burn_mode)) + .on_action( + cx.listener(|this, _: &KeepAll, window, cx| this.handle_accept_all(window, cx)), + ) + .on_action( + cx.listener(|this, _: &RejectAll, window, cx| this.handle_reject_all(window, cx)), + ) .capture_action(cx.listener(Self::paste)) .gap_2() .p_2() @@ -870,7 +919,10 @@ impl MessageEditor { let bg_edit_files_disclosure = editor_bg_color.blend(active_color.opacity(0.3)); let is_edit_changes_expanded = self.edits_expanded; - let is_generating = self.thread.read(cx).is_generating(); + let thread = self.thread.read(cx); + let pending_edits = thread.has_pending_edit_tool_uses(); + + const EDIT_NOT_READY_TOOLTIP_LABEL: &str = "Wait until file edits are complete."; v_flex() .mt_1() @@ -888,31 +940,28 @@ impl MessageEditor { }]) .child( h_flex() - .id("edits-container") - .cursor_pointer() - .p_1p5() + .p_1() .justify_between() .when(is_edit_changes_expanded, |this| { this.border_b_1().border_color(border_color) }) - .on_click( - cx.listener(|this, _, window, cx| this.handle_review_click(window, cx)), - ) .child( h_flex() + .id("edits-container") + .cursor_pointer() + .w_full() .gap_1() .child( Disclosure::new("edits-disclosure", is_edit_changes_expanded) - .on_click(cx.listener(|this, _ev, _window, cx| { - this.edits_expanded = !this.edits_expanded; - cx.notify(); + .on_click(cx.listener(|this, _, _, cx| { + this.handle_edit_bar_expand(cx) })), ) .map(|this| { - if is_generating { + if pending_edits { this.child( - AnimatedLabel::new(format!( - "Editing {} {}", + Label::new(format!( + "Editing {} {}…", changed_buffers.len(), if changed_buffers.len() == 1 { "file" @@ -920,7 +969,15 @@ impl MessageEditor { "files" } )) - .size(LabelSize::Small), + .color(Color::Muted) + .size(LabelSize::Small) + .with_animation( + "edit-label", + Animation::new(Duration::from_secs(2)) + .repeat() + .with_easing(pulsating_between(0.3, 0.7)), + |label, delta| label.alpha(delta), + ), ) } else { this.child( @@ -945,23 +1002,74 @@ impl MessageEditor { .color(Color::Muted), ) } - }), + }) + .on_click( + cx.listener(|this, _, _, cx| this.handle_edit_bar_expand(cx)), + ), ) .child( - Button::new("review", "Review Changes") - .label_size(LabelSize::Small) - .key_binding( - KeyBinding::for_action_in( - &OpenAgentDiff, - &focus_handle, - window, - cx, - ) - .map(|kb| kb.size(rems_from_px(12.))), + h_flex() + .gap_1() + .child( + IconButton::new("review-changes", IconName::ListTodo) + .icon_size(IconSize::Small) + .tooltip({ + let focus_handle = focus_handle.clone(); + move |window, cx| { + Tooltip::for_action_in( + "Review Changes", + &OpenAgentDiff, + &focus_handle, + window, + cx, + ) + } + }) + .on_click(cx.listener(|this, _, window, cx| { + this.handle_review_click(window, cx) + })), + ) + .child(ui::Divider::vertical().color(ui::DividerColor::Border)) + .child( + Button::new("reject-all-changes", "Reject All") + .label_size(LabelSize::Small) + .disabled(pending_edits) + .when(pending_edits, |this| { + this.tooltip(Tooltip::text(EDIT_NOT_READY_TOOLTIP_LABEL)) + }) + .key_binding( + KeyBinding::for_action_in( + &RejectAll, + &focus_handle.clone(), + window, + cx, + ) + .map(|kb| kb.size(rems_from_px(10.))), + ) + .on_click(cx.listener(|this, _, window, cx| { + this.handle_reject_all(window, cx) + })), ) - .on_click(cx.listener(|this, _, window, cx| { - this.handle_review_click(window, cx) - })), + .child( + Button::new("accept-all-changes", "Accept All") + .label_size(LabelSize::Small) + .disabled(pending_edits) + .when(pending_edits, |this| { + this.tooltip(Tooltip::text(EDIT_NOT_READY_TOOLTIP_LABEL)) + }) + .key_binding( + KeyBinding::for_action_in( + &KeepAll, + &focus_handle, + window, + cx, + ) + .map(|kb| kb.size(rems_from_px(10.))), + ) + .on_click(cx.listener(|this, _, window, cx| { + this.handle_accept_all(window, cx) + })), + ), ), ) .when(is_edit_changes_expanded, |parent| { diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index f907766759d54bc2250ee8820c961958feafb30d..daa7d5726f959f18cad65fc2af7d5c5a91571d9c 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -871,7 +871,16 @@ impl Thread { self.tool_use .pending_tool_uses() .iter() - .all(|tool_use| tool_use.status.is_error()) + .all(|pending_tool_use| pending_tool_use.status.is_error()) + } + + /// Returns whether any pending tool uses may perform edits + pub fn has_pending_edit_tool_uses(&self) -> bool { + self.tool_use + .pending_tool_uses() + .iter() + .filter(|pending_tool_use| !pending_tool_use.status.is_error()) + .any(|pending_tool_use| pending_tool_use.may_perform_edits) } pub fn tool_uses_for_message(&self, id: MessageId, cx: &App) -> Vec { diff --git a/crates/agent/src/tool_use.rs b/crates/agent/src/tool_use.rs index c26968949f19283873e7ee61e84cf3e4c59f0aaf..da6adc07f0c3c81ed4033c9d25e04438f440277a 100644 --- a/crates/agent/src/tool_use.rs +++ b/crates/agent/src/tool_use.rs @@ -337,6 +337,12 @@ impl ToolUseState { ) .into(); + let may_perform_edits = self + .tools + .read(cx) + .tool(&tool_use.name, cx) + .is_some_and(|tool| tool.may_perform_edits()); + self.pending_tool_uses_by_id.insert( tool_use.id.clone(), PendingToolUse { @@ -345,6 +351,7 @@ impl ToolUseState { name: tool_use.name.clone(), ui_text: ui_text.clone(), input: tool_use.input, + may_perform_edits, status, }, ); @@ -518,6 +525,7 @@ pub struct PendingToolUse { pub ui_text: Arc, pub input: serde_json::Value, pub status: PendingToolUseStatus, + pub may_perform_edits: bool, } #[derive(Debug, Clone)] diff --git a/crates/assistant_tool/src/assistant_tool.rs b/crates/assistant_tool/src/assistant_tool.rs index ecda105f6dcb2bb3f3a6b7a530c6dfe4399b9a89..6c08a61cf4479dec6c643020dbcadb642e02cdd7 100644 --- a/crates/assistant_tool/src/assistant_tool.rs +++ b/crates/assistant_tool/src/assistant_tool.rs @@ -218,6 +218,9 @@ pub trait Tool: 'static + Send + Sync { /// before having permission to run. fn needs_confirmation(&self, input: &serde_json::Value, cx: &App) -> bool; + /// Returns true if the tool may perform edits. + fn may_perform_edits(&self) -> bool; + /// Returns the JSON schema that describes the tool's input. fn input_schema(&self, _: LanguageModelToolSchemaFormat) -> Result { Ok(serde_json::Value::Object(serde_json::Map::default())) diff --git a/crates/assistant_tools/src/copy_path_tool.rs b/crates/assistant_tools/src/copy_path_tool.rs index a27209b0d167b96b07c7426aa01043972911f6f0..28d6bef9dd899360cd08e28b876830f81a5bb50a 100644 --- a/crates/assistant_tools/src/copy_path_tool.rs +++ b/crates/assistant_tools/src/copy_path_tool.rs @@ -48,6 +48,10 @@ impl Tool for CopyPathTool { false } + fn may_perform_edits(&self) -> bool { + true + } + fn description(&self) -> String { include_str!("./copy_path_tool/description.md").into() } diff --git a/crates/assistant_tools/src/create_directory_tool.rs b/crates/assistant_tools/src/create_directory_tool.rs index 5d4b36c2e8b8828db92b18c179e82dfddd600a50..b3e198c1b5e276032846dc8a6c2b67b02c917379 100644 --- a/crates/assistant_tools/src/create_directory_tool.rs +++ b/crates/assistant_tools/src/create_directory_tool.rs @@ -33,12 +33,16 @@ impl Tool for CreateDirectoryTool { "create_directory".into() } + fn description(&self) -> String { + include_str!("./create_directory_tool/description.md").into() + } + fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool { false } - fn description(&self) -> String { - include_str!("./create_directory_tool/description.md").into() + fn may_perform_edits(&self) -> bool { + false } fn icon(&self) -> IconName { diff --git a/crates/assistant_tools/src/delete_path_tool.rs b/crates/assistant_tools/src/delete_path_tool.rs index 275161840b0998e8d76f108eaac86b910d079c3c..e45c1976d1f32642b4091e9fad75385a5b4a7c93 100644 --- a/crates/assistant_tools/src/delete_path_tool.rs +++ b/crates/assistant_tools/src/delete_path_tool.rs @@ -37,6 +37,10 @@ impl Tool for DeletePathTool { false } + fn may_perform_edits(&self) -> bool { + true + } + fn description(&self) -> String { include_str!("./delete_path_tool/description.md").into() } diff --git a/crates/assistant_tools/src/diagnostics_tool.rs b/crates/assistant_tools/src/diagnostics_tool.rs index 2cac59c2d97bfac38d9c86163e7bca787ff00994..3b6d38fc06c0e9f8b95f031cb900ace74c5c6b04 100644 --- a/crates/assistant_tools/src/diagnostics_tool.rs +++ b/crates/assistant_tools/src/diagnostics_tool.rs @@ -50,6 +50,10 @@ impl Tool for DiagnosticsTool { false } + fn may_perform_edits(&self) -> bool { + false + } + fn description(&self) -> String { include_str!("./diagnostics_tool/description.md").into() } diff --git a/crates/assistant_tools/src/edit_file_tool.rs b/crates/assistant_tools/src/edit_file_tool.rs index c4768934db21aaf4b257efd17a152778062203d4..bde904abb53bd28dfdcd25ea20ed7032b487bdb0 100644 --- a/crates/assistant_tools/src/edit_file_tool.rs +++ b/crates/assistant_tools/src/edit_file_tool.rs @@ -129,6 +129,10 @@ impl Tool for EditFileTool { false } + fn may_perform_edits(&self) -> bool { + true + } + fn description(&self) -> String { include_str!("edit_file_tool/description.md").to_string() } diff --git a/crates/assistant_tools/src/fetch_tool.rs b/crates/assistant_tools/src/fetch_tool.rs index 2c593407b6aa9c488769f539a6bd1aa83c630356..82b15b7a86905219167d4f4fb630e6c9bab2c79d 100644 --- a/crates/assistant_tools/src/fetch_tool.rs +++ b/crates/assistant_tools/src/fetch_tool.rs @@ -118,7 +118,11 @@ impl Tool for FetchTool { } fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool { - true + false + } + + fn may_perform_edits(&self) -> bool { + false } fn description(&self) -> String { diff --git a/crates/assistant_tools/src/find_path_tool.rs b/crates/assistant_tools/src/find_path_tool.rs index 1bf19d8d984bc154445c5a85d7e330bba3e0824c..86e67a8f58cd71aedd163e15cb95aeb9e3357a87 100644 --- a/crates/assistant_tools/src/find_path_tool.rs +++ b/crates/assistant_tools/src/find_path_tool.rs @@ -59,6 +59,10 @@ impl Tool for FindPathTool { false } + fn may_perform_edits(&self) -> bool { + false + } + fn description(&self) -> String { include_str!("./find_path_tool/description.md").into() } diff --git a/crates/assistant_tools/src/grep_tool.rs b/crates/assistant_tools/src/grep_tool.rs index 202e7620f29f9fe4b13bceec53b610354cca3cc6..1b0c69b74417f3a7659255571ffa5bafdbb1a5b1 100644 --- a/crates/assistant_tools/src/grep_tool.rs +++ b/crates/assistant_tools/src/grep_tool.rs @@ -60,6 +60,10 @@ impl Tool for GrepTool { false } + fn may_perform_edits(&self) -> bool { + false + } + fn description(&self) -> String { include_str!("./grep_tool/description.md").into() } diff --git a/crates/assistant_tools/src/list_directory_tool.rs b/crates/assistant_tools/src/list_directory_tool.rs index cfd024751415d4d7bef87cf5c72929d55bea1341..2c8bf0f6cf037b3267c64d6ecb96a52cbc29d933 100644 --- a/crates/assistant_tools/src/list_directory_tool.rs +++ b/crates/assistant_tools/src/list_directory_tool.rs @@ -48,6 +48,10 @@ impl Tool for ListDirectoryTool { false } + fn may_perform_edits(&self) -> bool { + false + } + fn description(&self) -> String { include_str!("./list_directory_tool/description.md").into() } diff --git a/crates/assistant_tools/src/move_path_tool.rs b/crates/assistant_tools/src/move_path_tool.rs index ec079b6a56ffe3f10d1877be0a5d6ac11f13a863..27ae10151d4e91f951e198e850e5ff6fc2fb331b 100644 --- a/crates/assistant_tools/src/move_path_tool.rs +++ b/crates/assistant_tools/src/move_path_tool.rs @@ -46,6 +46,10 @@ impl Tool for MovePathTool { false } + fn may_perform_edits(&self) -> bool { + true + } + fn description(&self) -> String { include_str!("./move_path_tool/description.md").into() } diff --git a/crates/assistant_tools/src/now_tool.rs b/crates/assistant_tools/src/now_tool.rs index 8587c9f7e686c3fbad735cfa5914a66ea1e125b5..b6b1cf90a43b487684b9c8f0d4f6a69a14af6455 100644 --- a/crates/assistant_tools/src/now_tool.rs +++ b/crates/assistant_tools/src/now_tool.rs @@ -37,6 +37,10 @@ impl Tool for NowTool { false } + fn may_perform_edits(&self) -> bool { + false + } + fn description(&self) -> String { "Returns the current datetime in RFC 3339 format. Only use this tool when the user specifically asks for it or the current task would benefit from knowing the current datetime.".into() } diff --git a/crates/assistant_tools/src/open_tool.rs b/crates/assistant_tools/src/open_tool.rs index 34d4a8bd075af03920f0905fcfb6b62d0ec56ffc..97a4769e19e60758fe509fab56bf7329ac7f30b6 100644 --- a/crates/assistant_tools/src/open_tool.rs +++ b/crates/assistant_tools/src/open_tool.rs @@ -26,7 +26,9 @@ impl Tool for OpenTool { fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool { true } - + fn may_perform_edits(&self) -> bool { + false + } fn description(&self) -> String { include_str!("./open_tool/description.md").to_string() } diff --git a/crates/assistant_tools/src/read_file_tool.rs b/crates/assistant_tools/src/read_file_tool.rs index 0be0b53d66bb0c7ace1c45d651e1e2f06363bf47..39cc3165d836f42c1fba6871ed1b2d13026e7096 100644 --- a/crates/assistant_tools/src/read_file_tool.rs +++ b/crates/assistant_tools/src/read_file_tool.rs @@ -58,6 +58,10 @@ impl Tool for ReadFileTool { false } + fn may_perform_edits(&self) -> bool { + false + } + fn description(&self) -> String { include_str!("./read_file_tool/description.md").into() } diff --git a/crates/assistant_tools/src/terminal_tool.rs b/crates/assistant_tools/src/terminal_tool.rs index 91a2d994eda499f7e5970201acf583eb711792d3..4059eac2cf53df80308841bd622e76f5998f58cf 100644 --- a/crates/assistant_tools/src/terminal_tool.rs +++ b/crates/assistant_tools/src/terminal_tool.rs @@ -80,6 +80,10 @@ impl Tool for TerminalTool { true } + fn may_perform_edits(&self) -> bool { + false + } + fn description(&self) -> String { include_str!("./terminal_tool/description.md").to_string() } diff --git a/crates/assistant_tools/src/thinking_tool.rs b/crates/assistant_tools/src/thinking_tool.rs index 1a8b6103ee6432787933b91fb8f5601b6df18f72..4641b7359e1039cefb80e2a4f97ec5db94bfd90e 100644 --- a/crates/assistant_tools/src/thinking_tool.rs +++ b/crates/assistant_tools/src/thinking_tool.rs @@ -28,6 +28,10 @@ impl Tool for ThinkingTool { false } + fn may_perform_edits(&self) -> bool { + false + } + fn description(&self) -> String { include_str!("./thinking_tool/description.md").to_string() } diff --git a/crates/assistant_tools/src/web_search_tool.rs b/crates/assistant_tools/src/web_search_tool.rs index 7478d2ba75754ffebba216e9842db9c845fac7f3..9430ac9d9e245d4f8871fcf120cba9ed48a5ba97 100644 --- a/crates/assistant_tools/src/web_search_tool.rs +++ b/crates/assistant_tools/src/web_search_tool.rs @@ -36,6 +36,10 @@ impl Tool for WebSearchTool { false } + fn may_perform_edits(&self) -> bool { + false + } + fn description(&self) -> String { "Search the web for information using your query. Use this when you need real-time information, facts, or data that might not be in your training. Results will include snippets and links from relevant web pages.".into() } diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs index adfbe1e52d8a250214e2178228b6155cf08b8afd..c7ea321dce539fab7b4fc78f2994f91c3d31795b 100644 --- a/crates/icons/src/icons.rs +++ b/crates/icons/src/icons.rs @@ -155,6 +155,7 @@ pub enum IconName { LineHeight, Link, ListCollapse, + ListTodo, ListTree, ListX, LoadCircle,