agent: Make read_file and edit_file tool call titles more specific (#37639)

Cole Miller created

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

Change summary

crates/agent2/src/tests/test_tools.rs              |  30 +
crates/agent2/src/thread.rs                        |  22 
crates/agent2/src/tools/context_server_registry.rs |   4 
crates/agent2/src/tools/copy_path_tool.rs          |   6 
crates/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 
crates/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(-)

Detailed changes

crates/agent2/src/tests/test_tools.rs 🔗

@@ -24,7 +24,11 @@ impl AgentTool for EchoTool {
         acp::ToolKind::Other
     }
 
-    fn initial_title(&self, _input: Result<Self::Input, serde_json::Value>) -> SharedString {
+    fn initial_title(
+        &self,
+        _input: Result<Self::Input, serde_json::Value>,
+        _cx: &mut App,
+    ) -> SharedString {
         "Echo".into()
     }
 
@@ -55,7 +59,11 @@ impl AgentTool for DelayTool {
         "delay"
     }
 
-    fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
+    fn initial_title(
+        &self,
+        input: Result<Self::Input, serde_json::Value>,
+        _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<Self::Input, serde_json::Value>) -> SharedString {
+    fn initial_title(
+        &self,
+        _input: Result<Self::Input, serde_json::Value>,
+        _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<Self::Input, serde_json::Value>) -> SharedString {
+    fn initial_title(
+        &self,
+        _input: Result<Self::Input, serde_json::Value>,
+        _cx: &mut App,
+    ) -> SharedString {
         "Infinite Tool".into()
     }
 
@@ -186,7 +202,11 @@ impl AgentTool for WordListTool {
         acp::ToolKind::Other
     }
 
-    fn initial_title(&self, _input: Result<Self::Input, serde_json::Value>) -> SharedString {
+    fn initial_title(
+        &self,
+        _input: Result<Self::Input, serde_json::Value>,
+        _cx: &mut App,
+    ) -> SharedString {
         "List of random words".into()
     }
 

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<Self::Input, serde_json::Value>) -> SharedString;
+    fn initial_title(
+        &self,
+        input: Result<Self::Input, serde_json::Value>,
+        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<serde_json::Value>;
     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<serde_json::Value> {

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?;

crates/agent2/src/tools/copy_path_tool.rs 🔗

@@ -58,7 +58,11 @@ impl AgentTool for CopyPathTool {
         ToolKind::Move
     }
 
-    fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> ui::SharedString {
+    fn initial_title(
+        &self,
+        input: Result<Self::Input, serde_json::Value>,
+        _cx: &mut App,
+    ) -> ui::SharedString {
         if let Ok(input) = input {
             let src = MarkdownInlineCode(&input.source_path);
             let dest = MarkdownInlineCode(&input.destination_path);

crates/agent2/src/tools/create_directory_tool.rs 🔗

@@ -49,7 +49,11 @@ impl AgentTool for CreateDirectoryTool {
         ToolKind::Read
     }
 
-    fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
+    fn initial_title(
+        &self,
+        input: Result<Self::Input, serde_json::Value>,
+        _cx: &mut App,
+    ) -> SharedString {
         if let Ok(input) = input {
             format!("Create directory {}", MarkdownInlineCode(&input.path)).into()
         } else {

crates/agent2/src/tools/delete_path_tool.rs 🔗

@@ -52,7 +52,11 @@ impl AgentTool for DeletePathTool {
         ToolKind::Delete
     }
 
-    fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
+    fn initial_title(
+        &self,
+        input: Result<Self::Input, serde_json::Value>,
+        _cx: &mut App,
+    ) -> SharedString {
         if let Ok(input) = input {
             format!("Delete “`{}`”", input.path).into()
         } else {

crates/agent2/src/tools/diagnostics_tool.rs 🔗

@@ -71,7 +71,11 @@ impl AgentTool for DiagnosticsTool {
         acp::ToolKind::Read
     }
 
-    fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
+    fn initial_title(
+        &self,
+        input: Result<Self::Input, serde_json::Value>,
+        _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,

crates/agent2/src/tools/edit_file_tool.rs 🔗

@@ -120,11 +120,17 @@ impl From<EditFileToolOutput> for LanguageModelToolResultContent {
 pub struct EditFileTool {
     thread: WeakEntity<Thread>,
     language_registry: Arc<LanguageRegistry>,
+    project: Entity<Project>,
 }
 
 impl EditFileTool {
-    pub fn new(thread: WeakEntity<Thread>, language_registry: Arc<LanguageRegistry>) -> Self {
+    pub fn new(
+        project: Entity<Project>,
+        thread: WeakEntity<Thread>,
+        language_registry: Arc<LanguageRegistry>,
+    ) -> Self {
         Self {
+            project,
             thread,
             language_registry,
         }
@@ -195,22 +201,50 @@ impl AgentTool for EditFileTool {
         acp::ToolKind::Edit
     }
 
-    fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
+    fn initial_title(
+        &self,
+        input: Result<Self::Input, serde_json::Value>,
+        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::<EditFileToolPartialInput>(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(

crates/agent2/src/tools/fetch_tool.rs 🔗

@@ -126,7 +126,11 @@ impl AgentTool for FetchTool {
         acp::ToolKind::Fetch
     }
 
-    fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
+    fn initial_title(
+        &self,
+        input: Result<Self::Input, serde_json::Value>,
+        _cx: &mut App,
+    ) -> SharedString {
         match input {
             Ok(input) => format!("Fetch {}", MarkdownEscaped(&input.url)).into(),
             Err(_) => "Fetch URL".into(),

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<Self::Input, serde_json::Value>) -> SharedString {
+    fn initial_title(
+        &self,
+        input: Result<Self::Input, serde_json::Value>,
+        _cx: &mut App,
+    ) -> SharedString {
         let mut title = "Find paths".to_string();
         if let Ok(input) = input {
             title.push_str(&format!(" matching “`{}`”", input.glob));

crates/agent2/src/tools/grep_tool.rs 🔗

@@ -75,7 +75,11 @@ impl AgentTool for GrepTool {
         acp::ToolKind::Search
     }
 
-    fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
+    fn initial_title(
+        &self,
+        input: Result<Self::Input, serde_json::Value>,
+        _cx: &mut App,
+    ) -> SharedString {
         match input {
             Ok(input) => {
                 let page = input.page();

crates/agent2/src/tools/list_directory_tool.rs 🔗

@@ -59,7 +59,11 @@ impl AgentTool for ListDirectoryTool {
         ToolKind::Read
     }
 
-    fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
+    fn initial_title(
+        &self,
+        input: Result<Self::Input, serde_json::Value>,
+        _cx: &mut App,
+    ) -> SharedString {
         if let Ok(input) = input {
             let path = MarkdownInlineCode(&input.path);
             format!("List the {path} directory's contents").into()

crates/agent2/src/tools/move_path_tool.rs 🔗

@@ -60,7 +60,11 @@ impl AgentTool for MovePathTool {
         ToolKind::Move
     }
 
-    fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
+    fn initial_title(
+        &self,
+        input: Result<Self::Input, serde_json::Value>,
+        _cx: &mut App,
+    ) -> SharedString {
         if let Ok(input) = input {
             let src = MarkdownInlineCode(&input.source_path);
             let dest = MarkdownInlineCode(&input.destination_path);

crates/agent2/src/tools/now_tool.rs 🔗

@@ -41,7 +41,11 @@ impl AgentTool for NowTool {
         acp::ToolKind::Other
     }
 
-    fn initial_title(&self, _input: Result<Self::Input, serde_json::Value>) -> SharedString {
+    fn initial_title(
+        &self,
+        _input: Result<Self::Input, serde_json::Value>,
+        _cx: &mut App,
+    ) -> SharedString {
         "Get current time".into()
     }
 

crates/agent2/src/tools/open_tool.rs 🔗

@@ -45,7 +45,11 @@ impl AgentTool for OpenTool {
         ToolKind::Execute
     }
 
-    fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
+    fn initial_title(
+        &self,
+        input: Result<Self::Input, serde_json::Value>,
+        _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<Result<Self::Output>> {
         // 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?;
 

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<Self::Input, serde_json::Value>) -> 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<Self::Input, serde_json::Value>,
+        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<ImageItem> = 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()
-                        })
-                    }
+                    })
                 }
             })?;
 

crates/agent2/src/tools/terminal_tool.rs 🔗

@@ -60,7 +60,11 @@ impl AgentTool for TerminalTool {
         acp::ToolKind::Execute
     }
 
-    fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
+    fn initial_title(
+        &self,
+        input: Result<Self::Input, serde_json::Value>,
+        _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?;
 

crates/agent2/src/tools/thinking_tool.rs 🔗

@@ -29,7 +29,11 @@ impl AgentTool for ThinkingTool {
         acp::ToolKind::Think
     }
 
-    fn initial_title(&self, _input: Result<Self::Input, serde_json::Value>) -> SharedString {
+    fn initial_title(
+        &self,
+        _input: Result<Self::Input, serde_json::Value>,
+        _cx: &mut App,
+    ) -> SharedString {
         "Thinking".into()
     }
 

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<Self::Input, serde_json::Value>) -> SharedString {
+    fn initial_title(
+        &self,
+        _input: Result<Self::Input, serde_json::Value>,
+        _cx: &mut App,
+    ) -> SharedString {
         "Searching the Web".into()
     }
 

crates/agent_ui/src/acp/thread_view.rs 🔗

@@ -2024,35 +2024,34 @@ impl AcpThreadView {
         window: &Window,
         cx: &Context<Self>,
     ) -> 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);

crates/markdown/src/markdown.rs 🔗

@@ -69,6 +69,7 @@ pub struct MarkdownStyle {
     pub heading_level_styles: Option<HeadingLevelStyles>,
     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();

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<PathBuf> {
+        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<ProjectPath> {
         self.find_worktree(abs_path, cx)
             .map(|(worktree, relative_path)| ProjectPath {