diff --git a/assets/icons/list_todo.svg b/assets/icons/list_todo.svg
new file mode 100644
index 0000000000..1f50219418
--- /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 0b463266f5..9012c1b092 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 75d35f3ed3..05aa67f8a7 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 68ffefb126..e4461f94de 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 9e3467cca6..484e91abfd 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)
+ })),
)
- .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)
+ })),
+ )
+ .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 f907766759..daa7d5726f 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 c26968949f..da6adc07f0 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 ecda105f6d..6c08a61cf4 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 a27209b0d1..28d6bef9dd 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 5d4b36c2e8..b3e198c1b5 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 275161840b..e45c1976d1 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 2cac59c2d9..3b6d38fc06 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 c4768934db..bde904abb5 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 2c593407b6..82b15b7a86 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 1bf19d8d98..86e67a8f58 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 202e7620f2..1b0c69b744 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 cfd0247514..2c8bf0f6cf 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 ec079b6a56..27ae10151d 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 8587c9f7e6..b6b1cf90a4 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 34d4a8bd07..97a4769e19 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 0be0b53d66..39cc3165d8 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 91a2d994ed..4059eac2cf 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 1a8b6103ee..4641b7359e 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 7478d2ba75..9430ac9d9e 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 adfbe1e52d..c7ea321dce 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,