Detailed changes
@@ -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()
}
@@ -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> {
@@ -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?;
@@ -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);
@@ -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 {
@@ -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 {
@@ -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,
@@ -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(
@@ -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(),
@@ -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));
@@ -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();
@@ -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()
@@ -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);
@@ -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()
}
@@ -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?;
@@ -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()
- })
- }
+ })
}
})?;
@@ -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?;
@@ -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()
}
@@ -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()
}
@@ -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);
@@ -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();
@@ -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 {