From 5f01f6d75fafce8012974f961645bc01e318c45f Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Mon, 8 Sep 2025 12:57:22 -0400 Subject: [PATCH] agent: Make read_file and edit_file tool call titles more specific (#37639) For read_file and edit_file, show the worktree-relative path if there's only one visible worktree, and the "full path" otherwise. Also restores the display of line numbers for read_file calls. Release Notes: - N/A --- crates/agent2/src/tests/test_tools.rs | 30 ++- crates/agent2/src/thread.rs | 22 +- .../src/tools/context_server_registry.rs | 4 +- crates/agent2/src/tools/copy_path_tool.rs | 6 +- .../agent2/src/tools/create_directory_tool.rs | 6 +- crates/agent2/src/tools/delete_path_tool.rs | 6 +- crates/agent2/src/tools/diagnostics_tool.rs | 6 +- crates/agent2/src/tools/edit_file_tool.rs | 244 ++++++++++++------ crates/agent2/src/tools/fetch_tool.rs | 6 +- crates/agent2/src/tools/find_path_tool.rs | 6 +- crates/agent2/src/tools/grep_tool.rs | 6 +- .../agent2/src/tools/list_directory_tool.rs | 6 +- crates/agent2/src/tools/move_path_tool.rs | 6 +- crates/agent2/src/tools/now_tool.rs | 6 +- crates/agent2/src/tools/open_tool.rs | 8 +- crates/agent2/src/tools/read_file_tool.rs | 89 ++++--- crates/agent2/src/tools/terminal_tool.rs | 8 +- crates/agent2/src/tools/thinking_tool.rs | 6 +- crates/agent2/src/tools/web_search_tool.rs | 6 +- crates/agent_ui/src/acp/thread_view.rs | 56 ++-- crates/markdown/src/markdown.rs | 16 +- crates/project/src/project.rs | 17 ++ 22 files changed, 393 insertions(+), 173 deletions(-) diff --git a/crates/agent2/src/tests/test_tools.rs b/crates/agent2/src/tests/test_tools.rs index 27be7b6ac384219cdd06e6dc971078c3ff0b9a7b..2275d23c2f8a924efce2d2d4d8bcf6a6f3a59def 100644 --- a/crates/agent2/src/tests/test_tools.rs +++ b/crates/agent2/src/tests/test_tools.rs @@ -24,7 +24,11 @@ impl AgentTool for EchoTool { acp::ToolKind::Other } - fn initial_title(&self, _input: Result) -> SharedString { + fn initial_title( + &self, + _input: Result, + _cx: &mut App, + ) -> SharedString { "Echo".into() } @@ -55,7 +59,11 @@ impl AgentTool for DelayTool { "delay" } - fn initial_title(&self, input: Result) -> SharedString { + fn initial_title( + &self, + input: Result, + _cx: &mut App, + ) -> SharedString { if let Ok(input) = input { format!("Delay {}ms", input.ms).into() } else { @@ -100,7 +108,11 @@ impl AgentTool for ToolRequiringPermission { acp::ToolKind::Other } - fn initial_title(&self, _input: Result) -> SharedString { + fn initial_title( + &self, + _input: Result, + _cx: &mut App, + ) -> SharedString { "This tool requires permission".into() } @@ -135,7 +147,11 @@ impl AgentTool for InfiniteTool { acp::ToolKind::Other } - fn initial_title(&self, _input: Result) -> SharedString { + fn initial_title( + &self, + _input: Result, + _cx: &mut App, + ) -> SharedString { "Infinite Tool".into() } @@ -186,7 +202,11 @@ impl AgentTool for WordListTool { acp::ToolKind::Other } - fn initial_title(&self, _input: Result) -> SharedString { + fn initial_title( + &self, + _input: Result, + _cx: &mut App, + ) -> SharedString { "List of random words".into() } diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index 6421e4982e9fef67af3c61f54f3374d59172f807..20c4cd07533b7cf9bd1dd00e666bbb66552db9d7 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -741,7 +741,7 @@ impl Thread { return; }; - let title = tool.initial_title(tool_use.input.clone()); + let title = tool.initial_title(tool_use.input.clone(), cx); let kind = tool.kind(); stream.send_tool_call(&tool_use.id, title, kind, tool_use.input.clone()); @@ -1062,7 +1062,11 @@ impl Thread { self.action_log.clone(), )); self.add_tool(DiagnosticsTool::new(self.project.clone())); - self.add_tool(EditFileTool::new(cx.weak_entity(), language_registry)); + self.add_tool(EditFileTool::new( + self.project.clone(), + cx.weak_entity(), + language_registry, + )); self.add_tool(FetchTool::new(self.project.read(cx).client().http_client())); self.add_tool(FindPathTool::new(self.project.clone())); self.add_tool(GrepTool::new(self.project.clone())); @@ -1514,7 +1518,7 @@ impl Thread { let mut title = SharedString::from(&tool_use.name); let mut kind = acp::ToolKind::Other; if let Some(tool) = tool.as_ref() { - title = tool.initial_title(tool_use.input.clone()); + title = tool.initial_title(tool_use.input.clone(), cx); kind = tool.kind(); } @@ -2148,7 +2152,11 @@ where fn kind() -> acp::ToolKind; /// The initial tool title to display. Can be updated during the tool run. - fn initial_title(&self, input: Result) -> SharedString; + fn initial_title( + &self, + input: Result, + cx: &mut App, + ) -> SharedString; /// Returns the JSON schema that describes the tool's input. fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Schema { @@ -2196,7 +2204,7 @@ pub trait AnyAgentTool { fn name(&self) -> SharedString; fn description(&self) -> SharedString; fn kind(&self) -> acp::ToolKind; - fn initial_title(&self, input: serde_json::Value) -> SharedString; + fn initial_title(&self, input: serde_json::Value, _cx: &mut App) -> SharedString; fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result; fn supported_provider(&self, _provider: &LanguageModelProviderId) -> bool { true @@ -2232,9 +2240,9 @@ where T::kind() } - fn initial_title(&self, input: serde_json::Value) -> SharedString { + fn initial_title(&self, input: serde_json::Value, _cx: &mut App) -> SharedString { let parsed_input = serde_json::from_value(input.clone()).map_err(|_| input); - self.0.initial_title(parsed_input) + self.0.initial_title(parsed_input, _cx) } fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result { diff --git a/crates/agent2/src/tools/context_server_registry.rs b/crates/agent2/src/tools/context_server_registry.rs index e13f47fb2399d7408c5047ff6491ce2d2e76d948..46fa0298044de017464dc1a2e5bd21bf57c1bfcf 100644 --- a/crates/agent2/src/tools/context_server_registry.rs +++ b/crates/agent2/src/tools/context_server_registry.rs @@ -145,7 +145,7 @@ impl AnyAgentTool for ContextServerTool { ToolKind::Other } - fn initial_title(&self, _input: serde_json::Value) -> SharedString { + fn initial_title(&self, _input: serde_json::Value, _cx: &mut App) -> SharedString { format!("Run MCP tool `{}`", self.tool.name).into() } @@ -176,7 +176,7 @@ impl AnyAgentTool for ContextServerTool { return Task::ready(Err(anyhow!("Context server not found"))); }; let tool_name = self.tool.name.clone(); - let authorize = event_stream.authorize(self.initial_title(input.clone()), cx); + let authorize = event_stream.authorize(self.initial_title(input.clone(), cx), cx); cx.spawn(async move |_cx| { authorize.await?; diff --git a/crates/agent2/src/tools/copy_path_tool.rs b/crates/agent2/src/tools/copy_path_tool.rs index 819a6ff20931a42f892d60df91f665aac3694401..8fcd80391f828c7503701a86e9e1b400115763d6 100644 --- a/crates/agent2/src/tools/copy_path_tool.rs +++ b/crates/agent2/src/tools/copy_path_tool.rs @@ -58,7 +58,11 @@ impl AgentTool for CopyPathTool { ToolKind::Move } - fn initial_title(&self, input: Result) -> ui::SharedString { + fn initial_title( + &self, + input: Result, + _cx: &mut App, + ) -> ui::SharedString { if let Ok(input) = input { let src = MarkdownInlineCode(&input.source_path); let dest = MarkdownInlineCode(&input.destination_path); diff --git a/crates/agent2/src/tools/create_directory_tool.rs b/crates/agent2/src/tools/create_directory_tool.rs index 652363d5fa2320819076f5465b701eec04d9cd9f..30bd6418db35182358ed6139a9078e40a29dfac5 100644 --- a/crates/agent2/src/tools/create_directory_tool.rs +++ b/crates/agent2/src/tools/create_directory_tool.rs @@ -49,7 +49,11 @@ impl AgentTool for CreateDirectoryTool { ToolKind::Read } - fn initial_title(&self, input: Result) -> SharedString { + fn initial_title( + &self, + input: Result, + _cx: &mut App, + ) -> SharedString { if let Ok(input) = input { format!("Create directory {}", MarkdownInlineCode(&input.path)).into() } else { diff --git a/crates/agent2/src/tools/delete_path_tool.rs b/crates/agent2/src/tools/delete_path_tool.rs index 0f9641127f1ffdbfec72d6d404acf5186a2bf12f..01a77f5d811127b3df470ec73fbc91ff7c26fd52 100644 --- a/crates/agent2/src/tools/delete_path_tool.rs +++ b/crates/agent2/src/tools/delete_path_tool.rs @@ -52,7 +52,11 @@ impl AgentTool for DeletePathTool { ToolKind::Delete } - fn initial_title(&self, input: Result) -> SharedString { + fn initial_title( + &self, + input: Result, + _cx: &mut App, + ) -> SharedString { if let Ok(input) = input { format!("Delete “`{}`”", input.path).into() } else { diff --git a/crates/agent2/src/tools/diagnostics_tool.rs b/crates/agent2/src/tools/diagnostics_tool.rs index 558bb918ced71a1777dde919a59de9eab4129d45..a38e317d43cb16d8ee652f1a5f7aabd8b1ce4c8f 100644 --- a/crates/agent2/src/tools/diagnostics_tool.rs +++ b/crates/agent2/src/tools/diagnostics_tool.rs @@ -71,7 +71,11 @@ impl AgentTool for DiagnosticsTool { acp::ToolKind::Read } - fn initial_title(&self, input: Result) -> SharedString { + fn initial_title( + &self, + input: Result, + _cx: &mut App, + ) -> SharedString { if let Some(path) = input.ok().and_then(|input| match input.path { Some(path) if !path.is_empty() => Some(path), _ => None, diff --git a/crates/agent2/src/tools/edit_file_tool.rs b/crates/agent2/src/tools/edit_file_tool.rs index ae37dc1f1340f9aa25789930b8f792ed8c3c8356..9237961bce513d740989c7e3076395ed68473859 100644 --- a/crates/agent2/src/tools/edit_file_tool.rs +++ b/crates/agent2/src/tools/edit_file_tool.rs @@ -120,11 +120,17 @@ impl From for LanguageModelToolResultContent { pub struct EditFileTool { thread: WeakEntity, language_registry: Arc, + project: Entity, } impl EditFileTool { - pub fn new(thread: WeakEntity, language_registry: Arc) -> Self { + pub fn new( + project: Entity, + thread: WeakEntity, + language_registry: Arc, + ) -> Self { Self { + project, thread, language_registry, } @@ -195,22 +201,50 @@ impl AgentTool for EditFileTool { acp::ToolKind::Edit } - fn initial_title(&self, input: Result) -> SharedString { + fn initial_title( + &self, + input: Result, + cx: &mut App, + ) -> SharedString { match input { - Ok(input) => input.display_description.into(), + Ok(input) => self + .project + .read(cx) + .find_project_path(&input.path, cx) + .and_then(|project_path| { + self.project + .read(cx) + .short_full_path_for_project_path(&project_path, cx) + }) + .unwrap_or(Path::new(&input.path).into()) + .to_string_lossy() + .to_string() + .into(), Err(raw_input) => { if let Some(input) = serde_json::from_value::(raw_input).ok() { + let path = input.path.trim(); + if !path.is_empty() { + return self + .project + .read(cx) + .find_project_path(&input.path, cx) + .and_then(|project_path| { + self.project + .read(cx) + .short_full_path_for_project_path(&project_path, cx) + }) + .unwrap_or(Path::new(&input.path).into()) + .to_string_lossy() + .to_string() + .into(); + } + let description = input.display_description.trim(); if !description.is_empty() { return description.to_string().into(); } - - let path = input.path.trim().to_string(); - if !path.is_empty() { - return path.into(); - } } DEFAULT_UI_TEXT.into() @@ -545,7 +579,7 @@ mod tests { let model = Arc::new(FakeLanguageModel::default()); let thread = cx.new(|cx| { Thread::new( - project, + project.clone(), cx.new(|_cx| ProjectContext::default()), context_server_registry, Templates::new(), @@ -560,11 +594,12 @@ mod tests { path: "root/nonexistent_file.txt".into(), mode: EditFileMode::Edit, }; - Arc::new(EditFileTool::new(thread.downgrade(), language_registry)).run( - input, - ToolCallEventStream::test().0, - cx, - ) + Arc::new(EditFileTool::new( + project, + thread.downgrade(), + language_registry, + )) + .run(input, ToolCallEventStream::test().0, cx) }) .await; assert_eq!( @@ -743,7 +778,7 @@ mod tests { let model = Arc::new(FakeLanguageModel::default()); let thread = cx.new(|cx| { Thread::new( - project, + project.clone(), cx.new(|_cx| ProjectContext::default()), context_server_registry, Templates::new(), @@ -775,6 +810,7 @@ mod tests { mode: EditFileMode::Overwrite, }; Arc::new(EditFileTool::new( + project.clone(), thread.downgrade(), language_registry.clone(), )) @@ -833,11 +869,12 @@ mod tests { path: "root/src/main.rs".into(), mode: EditFileMode::Overwrite, }; - Arc::new(EditFileTool::new(thread.downgrade(), language_registry)).run( - input, - ToolCallEventStream::test().0, - cx, - ) + Arc::new(EditFileTool::new( + project.clone(), + thread.downgrade(), + language_registry, + )) + .run(input, ToolCallEventStream::test().0, cx) }); // Stream the unformatted content @@ -885,7 +922,7 @@ mod tests { let model = Arc::new(FakeLanguageModel::default()); let thread = cx.new(|cx| { Thread::new( - project, + project.clone(), cx.new(|_cx| ProjectContext::default()), context_server_registry, Templates::new(), @@ -918,6 +955,7 @@ mod tests { mode: EditFileMode::Overwrite, }; Arc::new(EditFileTool::new( + project.clone(), thread.downgrade(), language_registry.clone(), )) @@ -969,11 +1007,12 @@ mod tests { path: "root/src/main.rs".into(), mode: EditFileMode::Overwrite, }; - Arc::new(EditFileTool::new(thread.downgrade(), language_registry)).run( - input, - ToolCallEventStream::test().0, - cx, - ) + Arc::new(EditFileTool::new( + project.clone(), + thread.downgrade(), + language_registry, + )) + .run(input, ToolCallEventStream::test().0, cx) }); // Stream the content with trailing whitespace @@ -1012,7 +1051,7 @@ mod tests { let model = Arc::new(FakeLanguageModel::default()); let thread = cx.new(|cx| { Thread::new( - project, + project.clone(), cx.new(|_cx| ProjectContext::default()), context_server_registry, Templates::new(), @@ -1020,7 +1059,11 @@ mod tests { cx, ) }); - let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry)); + let tool = Arc::new(EditFileTool::new( + project.clone(), + thread.downgrade(), + language_registry, + )); fs.insert_tree("/root", json!({})).await; // Test 1: Path with .zed component should require confirmation @@ -1148,7 +1191,7 @@ mod tests { let model = Arc::new(FakeLanguageModel::default()); let thread = cx.new(|cx| { Thread::new( - project, + project.clone(), cx.new(|_cx| ProjectContext::default()), context_server_registry, Templates::new(), @@ -1156,7 +1199,11 @@ mod tests { cx, ) }); - let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry)); + let tool = Arc::new(EditFileTool::new( + project.clone(), + thread.downgrade(), + language_registry, + )); // Test global config paths - these should require confirmation if they exist and are outside the project let test_cases = vec![ @@ -1264,7 +1311,11 @@ mod tests { cx, ) }); - let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry)); + let tool = Arc::new(EditFileTool::new( + project.clone(), + thread.downgrade(), + language_registry, + )); // Test files in different worktrees let test_cases = vec![ @@ -1344,7 +1395,11 @@ mod tests { cx, ) }); - let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry)); + let tool = Arc::new(EditFileTool::new( + project.clone(), + thread.downgrade(), + language_registry, + )); // Test edge cases let test_cases = vec![ @@ -1427,7 +1482,11 @@ mod tests { cx, ) }); - let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry)); + let tool = Arc::new(EditFileTool::new( + project.clone(), + thread.downgrade(), + language_registry, + )); // Test different EditFileMode values let modes = vec![ @@ -1507,48 +1566,67 @@ mod tests { cx, ) }); - let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry)); + let tool = Arc::new(EditFileTool::new( + project, + thread.downgrade(), + language_registry, + )); - assert_eq!( - tool.initial_title(Err(json!({ - "path": "src/main.rs", - "display_description": "", - "old_string": "old code", - "new_string": "new code" - }))), - "src/main.rs" - ); - assert_eq!( - tool.initial_title(Err(json!({ - "path": "", - "display_description": "Fix error handling", - "old_string": "old code", - "new_string": "new code" - }))), - "Fix error handling" - ); - assert_eq!( - tool.initial_title(Err(json!({ - "path": "src/main.rs", - "display_description": "Fix error handling", - "old_string": "old code", - "new_string": "new code" - }))), - "Fix error handling" - ); - assert_eq!( - tool.initial_title(Err(json!({ - "path": "", - "display_description": "", - "old_string": "old code", - "new_string": "new code" - }))), - DEFAULT_UI_TEXT - ); - assert_eq!( - tool.initial_title(Err(serde_json::Value::Null)), - DEFAULT_UI_TEXT - ); + cx.update(|cx| { + // ... + assert_eq!( + tool.initial_title( + Err(json!({ + "path": "src/main.rs", + "display_description": "", + "old_string": "old code", + "new_string": "new code" + })), + cx + ), + "src/main.rs" + ); + assert_eq!( + tool.initial_title( + Err(json!({ + "path": "", + "display_description": "Fix error handling", + "old_string": "old code", + "new_string": "new code" + })), + cx + ), + "Fix error handling" + ); + assert_eq!( + tool.initial_title( + Err(json!({ + "path": "src/main.rs", + "display_description": "Fix error handling", + "old_string": "old code", + "new_string": "new code" + })), + cx + ), + "src/main.rs" + ); + assert_eq!( + tool.initial_title( + Err(json!({ + "path": "", + "display_description": "", + "old_string": "old code", + "new_string": "new code" + })), + cx + ), + DEFAULT_UI_TEXT + ); + assert_eq!( + tool.initial_title(Err(serde_json::Value::Null), cx), + DEFAULT_UI_TEXT + ); + }); } #[gpui::test] @@ -1575,7 +1653,11 @@ mod tests { // Ensure the diff is finalized after the edit completes. { - let tool = Arc::new(EditFileTool::new(thread.downgrade(), languages.clone())); + let tool = Arc::new(EditFileTool::new( + project.clone(), + thread.downgrade(), + languages.clone(), + )); let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); let edit = cx.update(|cx| { tool.run( @@ -1600,7 +1682,11 @@ mod tests { // Ensure the diff is finalized if an error occurs while editing. { model.forbid_requests(); - let tool = Arc::new(EditFileTool::new(thread.downgrade(), languages.clone())); + let tool = Arc::new(EditFileTool::new( + project.clone(), + thread.downgrade(), + languages.clone(), + )); let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); let edit = cx.update(|cx| { tool.run( @@ -1623,7 +1709,11 @@ mod tests { // Ensure the diff is finalized if the tool call gets dropped. { - let tool = Arc::new(EditFileTool::new(thread.downgrade(), languages.clone())); + let tool = Arc::new(EditFileTool::new( + project.clone(), + thread.downgrade(), + languages.clone(), + )); let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); let edit = cx.update(|cx| { tool.run( diff --git a/crates/agent2/src/tools/fetch_tool.rs b/crates/agent2/src/tools/fetch_tool.rs index dd97271a799d11daf09e95147c18ab07d55e1caf..60654ac863acdc559aeaad90f1c73727f33d1b59 100644 --- a/crates/agent2/src/tools/fetch_tool.rs +++ b/crates/agent2/src/tools/fetch_tool.rs @@ -126,7 +126,11 @@ impl AgentTool for FetchTool { acp::ToolKind::Fetch } - fn initial_title(&self, input: Result) -> SharedString { + fn initial_title( + &self, + input: Result, + _cx: &mut App, + ) -> SharedString { match input { Ok(input) => format!("Fetch {}", MarkdownEscaped(&input.url)).into(), Err(_) => "Fetch URL".into(), diff --git a/crates/agent2/src/tools/find_path_tool.rs b/crates/agent2/src/tools/find_path_tool.rs index 384bd56e776d8814e668d6fe3104a394c63b639d..735ec67cffa31969e4eef741d6a23de05f3e15dc 100644 --- a/crates/agent2/src/tools/find_path_tool.rs +++ b/crates/agent2/src/tools/find_path_tool.rs @@ -93,7 +93,11 @@ impl AgentTool for FindPathTool { acp::ToolKind::Search } - fn initial_title(&self, input: Result) -> SharedString { + fn initial_title( + &self, + input: Result, + _cx: &mut App, + ) -> SharedString { let mut title = "Find paths".to_string(); if let Ok(input) = input { title.push_str(&format!(" matching “`{}`”", input.glob)); diff --git a/crates/agent2/src/tools/grep_tool.rs b/crates/agent2/src/tools/grep_tool.rs index b24e773903e76fe8e11287d054dd758670669ca2..e76a16bcc7f57b035fb1e2b09243d22230b52085 100644 --- a/crates/agent2/src/tools/grep_tool.rs +++ b/crates/agent2/src/tools/grep_tool.rs @@ -75,7 +75,11 @@ impl AgentTool for GrepTool { acp::ToolKind::Search } - fn initial_title(&self, input: Result) -> SharedString { + fn initial_title( + &self, + input: Result, + _cx: &mut App, + ) -> SharedString { match input { Ok(input) => { let page = input.page(); diff --git a/crates/agent2/src/tools/list_directory_tool.rs b/crates/agent2/src/tools/list_directory_tool.rs index e6fa8d743122ec117f4307a1c4a37ddd79bd574a..0fbe23fe205e6a9bd5a77e737460c17b997f9175 100644 --- a/crates/agent2/src/tools/list_directory_tool.rs +++ b/crates/agent2/src/tools/list_directory_tool.rs @@ -59,7 +59,11 @@ impl AgentTool for ListDirectoryTool { ToolKind::Read } - fn initial_title(&self, input: Result) -> SharedString { + fn initial_title( + &self, + input: Result, + _cx: &mut App, + ) -> SharedString { if let Ok(input) = input { let path = MarkdownInlineCode(&input.path); format!("List the {path} directory's contents").into() diff --git a/crates/agent2/src/tools/move_path_tool.rs b/crates/agent2/src/tools/move_path_tool.rs index d9fb60651b8cbb9d38713d660cf2e43070ef1f53..91880c1243e0aa48569ab8e6981ddd45b41ab411 100644 --- a/crates/agent2/src/tools/move_path_tool.rs +++ b/crates/agent2/src/tools/move_path_tool.rs @@ -60,7 +60,11 @@ impl AgentTool for MovePathTool { ToolKind::Move } - fn initial_title(&self, input: Result) -> SharedString { + fn initial_title( + &self, + input: Result, + _cx: &mut App, + ) -> SharedString { if let Ok(input) = input { let src = MarkdownInlineCode(&input.source_path); let dest = MarkdownInlineCode(&input.destination_path); diff --git a/crates/agent2/src/tools/now_tool.rs b/crates/agent2/src/tools/now_tool.rs index 49068be0dd91993ae7cc4d866617d399d754d529..3387c0a617017991f8b2590868864287f399ec28 100644 --- a/crates/agent2/src/tools/now_tool.rs +++ b/crates/agent2/src/tools/now_tool.rs @@ -41,7 +41,11 @@ impl AgentTool for NowTool { acp::ToolKind::Other } - fn initial_title(&self, _input: Result) -> SharedString { + fn initial_title( + &self, + _input: Result, + _cx: &mut App, + ) -> SharedString { "Get current time".into() } diff --git a/crates/agent2/src/tools/open_tool.rs b/crates/agent2/src/tools/open_tool.rs index df7b04c787df27cb8f4f1fccac0017b8d71994a8..595a9f380b752635f97ef5d1819a1140c1db8be0 100644 --- a/crates/agent2/src/tools/open_tool.rs +++ b/crates/agent2/src/tools/open_tool.rs @@ -45,7 +45,11 @@ impl AgentTool for OpenTool { ToolKind::Execute } - fn initial_title(&self, input: Result) -> SharedString { + fn initial_title( + &self, + input: Result, + _cx: &mut App, + ) -> SharedString { if let Ok(input) = input { format!("Open `{}`", MarkdownEscaped(&input.path_or_url)).into() } else { @@ -61,7 +65,7 @@ impl AgentTool for OpenTool { ) -> Task> { // If path_or_url turns out to be a path in the project, make it absolute. let abs_path = to_absolute_path(&input.path_or_url, self.project.clone(), cx); - let authorize = event_stream.authorize(self.initial_title(Ok(input.clone())), cx); + let authorize = event_stream.authorize(self.initial_title(Ok(input.clone()), cx), cx); cx.background_spawn(async move { authorize.await?; diff --git a/crates/agent2/src/tools/read_file_tool.rs b/crates/agent2/src/tools/read_file_tool.rs index e771c26eca6e453a8f3d4150079b31a839227a4d..99f145901c664624d66d7487cce579f55cff908a 100644 --- a/crates/agent2/src/tools/read_file_tool.rs +++ b/crates/agent2/src/tools/read_file_tool.rs @@ -10,7 +10,7 @@ use project::{AgentLocation, ImageItem, Project, WorktreeSettings, image_store}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::Settings; -use std::{path::Path, sync::Arc}; +use std::sync::Arc; use util::markdown::MarkdownCodeBlock; use crate::{AgentTool, ToolCallEventStream}; @@ -68,13 +68,31 @@ impl AgentTool for ReadFileTool { acp::ToolKind::Read } - fn initial_title(&self, input: Result) -> SharedString { - input - .ok() - .as_ref() - .and_then(|input| Path::new(&input.path).file_name()) - .map(|file_name| file_name.to_string_lossy().to_string().into()) - .unwrap_or_default() + fn initial_title( + &self, + input: Result, + cx: &mut App, + ) -> SharedString { + if let Ok(input) = input + && let Some(project_path) = self.project.read(cx).find_project_path(&input.path, cx) + && let Some(path) = self + .project + .read(cx) + .short_full_path_for_project_path(&project_path, cx) + { + match (input.start_line, input.end_line) { + (Some(start), Some(end)) => { + format!("Read file `{}` (lines {}-{})", path.display(), start, end,) + } + (Some(start), None) => { + format!("Read file `{}` (from line {})", path.display(), start) + } + _ => format!("Read file `{}`", path.display()), + } + .into() + } else { + "Read file".into() + } } fn run( @@ -86,6 +104,12 @@ impl AgentTool for ReadFileTool { let Some(project_path) = self.project.read(cx).find_project_path(&input.path, cx) else { return Task::ready(Err(anyhow!("Path {} not found in project", &input.path))); }; + let Some(abs_path) = self.project.read(cx).absolute_path(&project_path, cx) else { + return Task::ready(Err(anyhow!( + "Failed to convert {} to absolute path", + &input.path + ))); + }; // Error out if this path is either excluded or private in global settings let global_settings = WorktreeSettings::get_global(cx); @@ -121,6 +145,14 @@ impl AgentTool for ReadFileTool { let file_path = input.path.clone(); + event_stream.update_fields(ToolCallUpdateFields { + locations: Some(vec![acp::ToolCallLocation { + path: abs_path, + line: input.start_line.map(|line| line.saturating_sub(1)), + }]), + ..Default::default() + }); + if image_store::is_image_file(&self.project, &project_path, cx) { return cx.spawn(async move |cx| { let image_entity: Entity = cx @@ -229,34 +261,25 @@ impl AgentTool for ReadFileTool { }; project.update(cx, |project, cx| { - if let Some(abs_path) = project.absolute_path(&project_path, cx) { - project.set_agent_location( - Some(AgentLocation { - buffer: buffer.downgrade(), - position: anchor.unwrap_or(text::Anchor::MIN), - }), - cx, - ); + project.set_agent_location( + Some(AgentLocation { + buffer: buffer.downgrade(), + position: anchor.unwrap_or(text::Anchor::MIN), + }), + cx, + ); + if let Ok(LanguageModelToolResultContent::Text(text)) = &result { + let markdown = MarkdownCodeBlock { + tag: &input.path, + text, + } + .to_string(); event_stream.update_fields(ToolCallUpdateFields { - locations: Some(vec![acp::ToolCallLocation { - path: abs_path, - line: input.start_line.map(|line| line.saturating_sub(1)), + content: Some(vec![acp::ToolCallContent::Content { + content: markdown.into(), }]), ..Default::default() - }); - if let Ok(LanguageModelToolResultContent::Text(text)) = &result { - let markdown = MarkdownCodeBlock { - tag: &input.path, - text, - } - .to_string(); - event_stream.update_fields(ToolCallUpdateFields { - content: Some(vec![acp::ToolCallContent::Content { - content: markdown.into(), - }]), - ..Default::default() - }) - } + }) } })?; diff --git a/crates/agent2/src/tools/terminal_tool.rs b/crates/agent2/src/tools/terminal_tool.rs index 9ed585b1386e4958fe8d458a0376a70e0ef70862..7acfc2455093eac0f3d15e840abce47f38a6c8b0 100644 --- a/crates/agent2/src/tools/terminal_tool.rs +++ b/crates/agent2/src/tools/terminal_tool.rs @@ -60,7 +60,11 @@ impl AgentTool for TerminalTool { acp::ToolKind::Execute } - fn initial_title(&self, input: Result) -> SharedString { + fn initial_title( + &self, + input: Result, + _cx: &mut App, + ) -> SharedString { if let Ok(input) = input { let mut lines = input.command.lines(); let first_line = lines.next().unwrap_or_default(); @@ -93,7 +97,7 @@ impl AgentTool for TerminalTool { Err(err) => return Task::ready(Err(err)), }; - let authorize = event_stream.authorize(self.initial_title(Ok(input.clone())), cx); + let authorize = event_stream.authorize(self.initial_title(Ok(input.clone()), cx), cx); cx.spawn(async move |cx| { authorize.await?; diff --git a/crates/agent2/src/tools/thinking_tool.rs b/crates/agent2/src/tools/thinking_tool.rs index 61fb9eb0d6ea95f1aa299f1d226c7f2c5b750767..0a68f7545f81ce3202c110b1435d33b57adf409c 100644 --- a/crates/agent2/src/tools/thinking_tool.rs +++ b/crates/agent2/src/tools/thinking_tool.rs @@ -29,7 +29,11 @@ impl AgentTool for ThinkingTool { acp::ToolKind::Think } - fn initial_title(&self, _input: Result) -> SharedString { + fn initial_title( + &self, + _input: Result, + _cx: &mut App, + ) -> SharedString { "Thinking".into() } diff --git a/crates/agent2/src/tools/web_search_tool.rs b/crates/agent2/src/tools/web_search_tool.rs index d7a34bec29e10476b31051d71d6d2f74b640ad5d..ce26bccddeeb998abf6d39cbe2acfe91cecc6d1b 100644 --- a/crates/agent2/src/tools/web_search_tool.rs +++ b/crates/agent2/src/tools/web_search_tool.rs @@ -48,7 +48,11 @@ impl AgentTool for WebSearchTool { acp::ToolKind::Fetch } - fn initial_title(&self, _input: Result) -> SharedString { + fn initial_title( + &self, + _input: Result, + _cx: &mut App, + ) -> SharedString { "Searching the Web".into() } diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index d635918c192a47feea2a8db84e6c21d37e2880d7..bd7a9a1479e763bd42b825a562f7ee4a7c85e57e 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -2024,35 +2024,34 @@ impl AcpThreadView { window: &Window, cx: &Context, ) -> Div { + let has_location = tool_call.locations.len() == 1; let card_header_id = SharedString::from("inner-tool-call-header"); - let tool_icon = - if tool_call.kind == acp::ToolKind::Edit && tool_call.locations.len() == 1 { - FileIcons::get_icon(&tool_call.locations[0].path, cx) - .map(Icon::from_path) - .unwrap_or(Icon::new(IconName::ToolPencil)) - } else { - Icon::new(match tool_call.kind { - acp::ToolKind::Read => IconName::ToolSearch, - acp::ToolKind::Edit => IconName::ToolPencil, - acp::ToolKind::Delete => IconName::ToolDeleteFile, - acp::ToolKind::Move => IconName::ArrowRightLeft, - acp::ToolKind::Search => IconName::ToolSearch, - acp::ToolKind::Execute => IconName::ToolTerminal, - acp::ToolKind::Think => IconName::ToolThink, - acp::ToolKind::Fetch => IconName::ToolWeb, - acp::ToolKind::Other => IconName::ToolHammer, - }) - } - .size(IconSize::Small) - .color(Color::Muted); + let tool_icon = if tool_call.kind == acp::ToolKind::Edit && has_location { + FileIcons::get_icon(&tool_call.locations[0].path, cx) + .map(Icon::from_path) + .unwrap_or(Icon::new(IconName::ToolPencil)) + } else { + Icon::new(match tool_call.kind { + acp::ToolKind::Read => IconName::ToolSearch, + acp::ToolKind::Edit => IconName::ToolPencil, + acp::ToolKind::Delete => IconName::ToolDeleteFile, + acp::ToolKind::Move => IconName::ArrowRightLeft, + acp::ToolKind::Search => IconName::ToolSearch, + acp::ToolKind::Execute => IconName::ToolTerminal, + acp::ToolKind::Think => IconName::ToolThink, + acp::ToolKind::Fetch => IconName::ToolWeb, + acp::ToolKind::Other => IconName::ToolHammer, + }) + } + .size(IconSize::Small) + .color(Color::Muted); let failed_or_canceled = match &tool_call.status { ToolCallStatus::Rejected | ToolCallStatus::Canceled | ToolCallStatus::Failed => true, _ => false, }; - let has_location = tool_call.locations.len() == 1; let needs_confirmation = matches!( tool_call.status, ToolCallStatus::WaitingForConfirmation { .. } @@ -2195,13 +2194,6 @@ impl AcpThreadView { .overflow_hidden() .child(tool_icon) .child(if has_location { - let name = tool_call.locations[0] - .path - .file_name() - .unwrap_or_default() - .display() - .to_string(); - h_flex() .id(("open-tool-call-location", entry_ix)) .w_full() @@ -2212,7 +2204,13 @@ impl AcpThreadView { this.text_color(cx.theme().colors().text_muted) } }) - .child(name) + .child(self.render_markdown( + tool_call.label.clone(), + MarkdownStyle { + prevent_mouse_interaction: true, + ..default_markdown_style(false, true, window, cx) + }, + )) .tooltip(Tooltip::text("Jump to File")) .on_click(cx.listener(move |this, _, window, cx| { this.open_tool_call_location(entry_ix, 0, window, cx); diff --git a/crates/markdown/src/markdown.rs b/crates/markdown/src/markdown.rs index 1f607a033ae08b67f1c2cb66d5ed9d9efd316971..4e1d3ac51e148439e57a4a1c305dabc31cbc2046 100644 --- a/crates/markdown/src/markdown.rs +++ b/crates/markdown/src/markdown.rs @@ -69,6 +69,7 @@ pub struct MarkdownStyle { pub heading_level_styles: Option, pub table_overflow_x_scroll: bool, pub height_is_multiple_of_line_height: bool, + pub prevent_mouse_interaction: bool, } impl Default for MarkdownStyle { @@ -89,6 +90,7 @@ impl Default for MarkdownStyle { heading_level_styles: None, table_overflow_x_scroll: false, height_is_multiple_of_line_height: false, + prevent_mouse_interaction: false, } } } @@ -575,16 +577,22 @@ impl MarkdownElement { window: &mut Window, cx: &mut App, ) { + if self.style.prevent_mouse_interaction { + return; + } + let is_hovering_link = hitbox.is_hovered(window) && !self.markdown.read(cx).selection.pending && rendered_text .link_for_position(window.mouse_position()) .is_some(); - if is_hovering_link { - window.set_cursor_style(CursorStyle::PointingHand, hitbox); - } else { - window.set_cursor_style(CursorStyle::IBeam, hitbox); + if !self.style.prevent_mouse_interaction { + if is_hovering_link { + window.set_cursor_style(CursorStyle::PointingHand, hitbox); + } else { + window.set_cursor_style(CursorStyle::IBeam, hitbox); + } } let on_open_url = self.on_url_click.take(); diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 6b85726aed1e9c37ecf0b76b129b0dbe6974d4b6..a2bd4ee7b7b5e69586f05a72727a23339b61c26b 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -4497,6 +4497,23 @@ impl Project { None } + /// If there's only one visible worktree, returns the given worktree-relative path with no prefix. + /// + /// Otherwise, returns the full path for the project path (obtained by prefixing the worktree-relative path with the name of the worktree). + pub fn short_full_path_for_project_path( + &self, + project_path: &ProjectPath, + cx: &App, + ) -> Option { + if self.visible_worktrees(cx).take(2).count() < 2 { + return Some(project_path.path.to_path_buf()); + } + self.worktree_for_id(project_path.worktree_id, cx) + .and_then(|worktree| { + Some(Path::new(worktree.read(cx).abs_path().file_name()?).join(&project_path.path)) + }) + } + pub fn project_path_for_absolute_path(&self, abs_path: &Path, cx: &App) -> Option { self.find_worktree(abs_path, cx) .map(|(worktree, relative_path)| ProjectPath {