Lay the groundwork to create terminals in AcpThread (#35872)

This just prepares the types so that it will be easy later to update a
tool call with a terminal entity. We paused because we realized we want
to simplify how terminals are created in zed, and so that warrants a
dedicated pull request that can be reviewed in isolation.

Release Notes:

- N/A

---------

Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
This commit is contained in:
Antonio Scandurra 2025-08-08 16:39:40 +02:00 committed by GitHub
parent 51298b6912
commit db901278f2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 292 additions and 143 deletions

View file

@ -198,7 +198,7 @@ impl ToolCall {
} }
} }
fn update( fn update_fields(
&mut self, &mut self,
fields: acp::ToolCallUpdateFields, fields: acp::ToolCallUpdateFields,
language_registry: Arc<LanguageRegistry>, language_registry: Arc<LanguageRegistry>,
@ -415,6 +415,39 @@ impl ToolCallContent {
} }
} }
#[derive(Debug, PartialEq)]
pub enum ToolCallUpdate {
UpdateFields(acp::ToolCallUpdate),
UpdateDiff(ToolCallUpdateDiff),
}
impl ToolCallUpdate {
fn id(&self) -> &acp::ToolCallId {
match self {
Self::UpdateFields(update) => &update.id,
Self::UpdateDiff(diff) => &diff.id,
}
}
}
impl From<acp::ToolCallUpdate> for ToolCallUpdate {
fn from(update: acp::ToolCallUpdate) -> Self {
Self::UpdateFields(update)
}
}
impl From<ToolCallUpdateDiff> for ToolCallUpdate {
fn from(diff: ToolCallUpdateDiff) -> Self {
Self::UpdateDiff(diff)
}
}
#[derive(Debug, PartialEq)]
pub struct ToolCallUpdateDiff {
pub id: acp::ToolCallId,
pub diff: Entity<Diff>,
}
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct Plan { pub struct Plan {
pub entries: Vec<PlanEntry>, pub entries: Vec<PlanEntry>,
@ -710,36 +743,32 @@ impl AcpThread {
pub fn update_tool_call( pub fn update_tool_call(
&mut self, &mut self,
update: acp::ToolCallUpdate, update: impl Into<ToolCallUpdate>,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Result<()> { ) -> Result<()> {
let update = update.into();
let languages = self.project.read(cx).languages().clone(); let languages = self.project.read(cx).languages().clone();
let (ix, current_call) = self let (ix, current_call) = self
.tool_call_mut(&update.id) .tool_call_mut(update.id())
.context("Tool call not found")?; .context("Tool call not found")?;
current_call.update(update.fields, languages, cx); match update {
ToolCallUpdate::UpdateFields(update) => {
current_call.update_fields(update.fields, languages, cx);
}
ToolCallUpdate::UpdateDiff(update) => {
current_call.content.clear();
current_call
.content
.push(ToolCallContent::Diff { diff: update.diff });
}
}
cx.emit(AcpThreadEvent::EntryUpdated(ix)); cx.emit(AcpThreadEvent::EntryUpdated(ix));
Ok(()) Ok(())
} }
pub fn set_tool_call_diff(
&mut self,
tool_call_id: &acp::ToolCallId,
diff: Entity<Diff>,
cx: &mut Context<Self>,
) -> Result<()> {
let (ix, current_call) = self
.tool_call_mut(tool_call_id)
.context("Tool call not found")?;
current_call.content.clear();
current_call.content.push(ToolCallContent::Diff { diff });
cx.emit(AcpThreadEvent::EntryUpdated(ix));
Ok(())
}
/// Updates a tool call if id matches an existing entry, otherwise inserts a new one. /// Updates a tool call if id matches an existing entry, otherwise inserts a new one.
pub fn upsert_tool_call(&mut self, tool_call: acp::ToolCall, cx: &mut Context<Self>) { pub fn upsert_tool_call(&mut self, tool_call: acp::ToolCall, cx: &mut Context<Self>) {
let status = ToolCallStatus::Allowed { let status = ToolCallStatus::Allowed {

View file

@ -503,29 +503,27 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
match event { match event {
AgentResponseEvent::Text(text) => { AgentResponseEvent::Text(text) => {
acp_thread.update(cx, |thread, cx| { acp_thread.update(cx, |thread, cx| {
thread.handle_session_update( thread.push_assistant_content_block(
acp::SessionUpdate::AgentMessageChunk { acp::ContentBlock::Text(acp::TextContent {
content: acp::ContentBlock::Text(acp::TextContent { text,
text, annotations: None,
annotations: None, }),
}), false,
},
cx, cx,
) )
})??; })?;
} }
AgentResponseEvent::Thinking(text) => { AgentResponseEvent::Thinking(text) => {
acp_thread.update(cx, |thread, cx| { acp_thread.update(cx, |thread, cx| {
thread.handle_session_update( thread.push_assistant_content_block(
acp::SessionUpdate::AgentThoughtChunk { acp::ContentBlock::Text(acp::TextContent {
content: acp::ContentBlock::Text(acp::TextContent { text,
text, annotations: None,
annotations: None, }),
}), true,
},
cx, cx,
) )
})??; })?;
} }
AgentResponseEvent::ToolCallAuthorization(ToolCallAuthorization { AgentResponseEvent::ToolCallAuthorization(ToolCallAuthorization {
tool_call, tool_call,
@ -551,27 +549,12 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
} }
AgentResponseEvent::ToolCall(tool_call) => { AgentResponseEvent::ToolCall(tool_call) => {
acp_thread.update(cx, |thread, cx| { acp_thread.update(cx, |thread, cx| {
thread.handle_session_update( thread.upsert_tool_call(tool_call, cx)
acp::SessionUpdate::ToolCall(tool_call), })?;
cx,
)
})??;
} }
AgentResponseEvent::ToolCallUpdate(tool_call_update) => { AgentResponseEvent::ToolCallUpdate(update) => {
acp_thread.update(cx, |thread, cx| { acp_thread.update(cx, |thread, cx| {
thread.handle_session_update( thread.update_tool_call(update, cx)
acp::SessionUpdate::ToolCallUpdate(tool_call_update),
cx,
)
})??;
}
AgentResponseEvent::ToolCallDiff(tool_call_diff) => {
acp_thread.update(cx, |thread, cx| {
thread.set_tool_call_diff(
&tool_call_diff.tool_call_id,
tool_call_diff.diff,
cx,
)
})??; })??;
} }
AgentResponseEvent::Stop(stop_reason) => { AgentResponseEvent::Stop(stop_reason) => {

View file

@ -306,7 +306,7 @@ async fn test_tool_hallucination(cx: &mut TestAppContext) {
let tool_call = expect_tool_call(&mut events).await; let tool_call = expect_tool_call(&mut events).await;
assert_eq!(tool_call.title, "nonexistent_tool"); assert_eq!(tool_call.title, "nonexistent_tool");
assert_eq!(tool_call.status, acp::ToolCallStatus::Pending); assert_eq!(tool_call.status, acp::ToolCallStatus::Pending);
let update = expect_tool_call_update(&mut events).await; let update = expect_tool_call_update_fields(&mut events).await;
assert_eq!(update.fields.status, Some(acp::ToolCallStatus::Failed)); assert_eq!(update.fields.status, Some(acp::ToolCallStatus::Failed));
} }
@ -326,7 +326,7 @@ async fn expect_tool_call(
} }
} }
async fn expect_tool_call_update( async fn expect_tool_call_update_fields(
events: &mut UnboundedReceiver<Result<AgentResponseEvent, LanguageModelCompletionError>>, events: &mut UnboundedReceiver<Result<AgentResponseEvent, LanguageModelCompletionError>>,
) -> acp::ToolCallUpdate { ) -> acp::ToolCallUpdate {
let event = events let event = events
@ -335,7 +335,9 @@ async fn expect_tool_call_update(
.expect("no tool call authorization event received") .expect("no tool call authorization event received")
.unwrap(); .unwrap();
match event { match event {
AgentResponseEvent::ToolCallUpdate(tool_call_update) => return tool_call_update, AgentResponseEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateFields(update)) => {
return update
}
event => { event => {
panic!("Unexpected event {event:?}"); panic!("Unexpected event {event:?}");
} }
@ -425,31 +427,33 @@ async fn test_cancellation(cx: &mut TestAppContext) {
}); });
// Wait until both tools are called. // Wait until both tools are called.
let mut expected_tool_calls = vec!["echo", "infinite"]; let mut expected_tools = vec!["Echo", "Infinite Tool"];
let mut echo_id = None; let mut echo_id = None;
let mut echo_completed = false; let mut echo_completed = false;
while let Some(event) = events.next().await { while let Some(event) = events.next().await {
match event.unwrap() { match event.unwrap() {
AgentResponseEvent::ToolCall(tool_call) => { AgentResponseEvent::ToolCall(tool_call) => {
assert_eq!(tool_call.title, expected_tool_calls.remove(0)); assert_eq!(tool_call.title, expected_tools.remove(0));
if tool_call.title == "echo" { if tool_call.title == "Echo" {
echo_id = Some(tool_call.id); echo_id = Some(tool_call.id);
} }
} }
AgentResponseEvent::ToolCallUpdate(acp::ToolCallUpdate { AgentResponseEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateFields(
id, acp::ToolCallUpdate {
fields: id,
acp::ToolCallUpdateFields { fields:
status: Some(acp::ToolCallStatus::Completed), acp::ToolCallUpdateFields {
.. status: Some(acp::ToolCallStatus::Completed),
}, ..
}) if Some(&id) == echo_id.as_ref() => { },
},
)) if Some(&id) == echo_id.as_ref() => {
echo_completed = true; echo_completed = true;
} }
_ => {} _ => {}
} }
if expected_tool_calls.is_empty() && echo_completed { if expected_tools.is_empty() && echo_completed {
break; break;
} }
} }
@ -647,13 +651,26 @@ async fn test_tool_updates_to_completion(cx: &mut TestAppContext) {
let mut events = thread.update(cx, |thread, cx| thread.send(model.clone(), "Think", cx)); let mut events = thread.update(cx, |thread, cx| thread.send(model.clone(), "Think", cx));
cx.run_until_parked(); cx.run_until_parked();
let input = json!({ "content": "Thinking hard!" }); // Simulate streaming partial input.
let input = json!({});
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
LanguageModelToolUse { LanguageModelToolUse {
id: "1".into(), id: "1".into(),
name: ThinkingTool.name().into(), name: ThinkingTool.name().into(),
raw_input: input.to_string(), raw_input: input.to_string(),
input, input,
is_input_complete: false,
},
));
// Input streaming completed
let input = json!({ "content": "Thinking hard!" });
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
LanguageModelToolUse {
id: "1".into(),
name: "thinking".into(),
raw_input: input.to_string(),
input,
is_input_complete: true, is_input_complete: true,
}, },
)); ));
@ -670,22 +687,35 @@ async fn test_tool_updates_to_completion(cx: &mut TestAppContext) {
status: acp::ToolCallStatus::Pending, status: acp::ToolCallStatus::Pending,
content: vec![], content: vec![],
locations: vec![], locations: vec![],
raw_input: Some(json!({ "content": "Thinking hard!" })), raw_input: Some(json!({})),
raw_output: None, raw_output: None,
} }
); );
let update = expect_tool_call_update(&mut events).await; let update = expect_tool_call_update_fields(&mut events).await;
assert_eq!( assert_eq!(
update, update,
acp::ToolCallUpdate { acp::ToolCallUpdate {
id: acp::ToolCallId("1".into()), id: acp::ToolCallId("1".into()),
fields: acp::ToolCallUpdateFields { fields: acp::ToolCallUpdateFields {
status: Some(acp::ToolCallStatus::InProgress,), title: Some("Thinking".into()),
kind: Some(acp::ToolKind::Think),
raw_input: Some(json!({ "content": "Thinking hard!" })),
..Default::default() ..Default::default()
}, },
} }
); );
let update = expect_tool_call_update(&mut events).await; let update = expect_tool_call_update_fields(&mut events).await;
assert_eq!(
update,
acp::ToolCallUpdate {
id: acp::ToolCallId("1".into()),
fields: acp::ToolCallUpdateFields {
status: Some(acp::ToolCallStatus::InProgress),
..Default::default()
},
}
);
let update = expect_tool_call_update_fields(&mut events).await;
assert_eq!( assert_eq!(
update, update,
acp::ToolCallUpdate { acp::ToolCallUpdate {
@ -696,7 +726,7 @@ async fn test_tool_updates_to_completion(cx: &mut TestAppContext) {
}, },
} }
); );
let update = expect_tool_call_update(&mut events).await; let update = expect_tool_call_update_fields(&mut events).await;
assert_eq!( assert_eq!(
update, update,
acp::ToolCallUpdate { acp::ToolCallUpdate {

View file

@ -24,7 +24,7 @@ impl AgentTool for EchoTool {
acp::ToolKind::Other acp::ToolKind::Other
} }
fn initial_title(&self, _: Self::Input) -> SharedString { fn initial_title(&self, _input: Result<Self::Input, serde_json::Value>) -> SharedString {
"Echo".into() "Echo".into()
} }
@ -55,8 +55,12 @@ impl AgentTool for DelayTool {
"delay".into() "delay".into()
} }
fn initial_title(&self, input: Self::Input) -> SharedString { fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
format!("Delay {}ms", input.ms).into() if let Ok(input) = input {
format!("Delay {}ms", input.ms).into()
} else {
"Delay".into()
}
} }
fn kind(&self) -> acp::ToolKind { fn kind(&self) -> acp::ToolKind {
@ -96,7 +100,7 @@ impl AgentTool for ToolRequiringPermission {
acp::ToolKind::Other acp::ToolKind::Other
} }
fn initial_title(&self, _input: Self::Input) -> SharedString { fn initial_title(&self, _input: Result<Self::Input, serde_json::Value>) -> SharedString {
"This tool requires permission".into() "This tool requires permission".into()
} }
@ -131,8 +135,8 @@ impl AgentTool for InfiniteTool {
acp::ToolKind::Other acp::ToolKind::Other
} }
fn initial_title(&self, _input: Self::Input) -> SharedString { fn initial_title(&self, _input: Result<Self::Input, serde_json::Value>) -> SharedString {
"This is the tool that never ends... it just goes on and on my friends!".into() "Infinite Tool".into()
} }
fn run( fn run(
@ -182,7 +186,7 @@ impl AgentTool for WordListTool {
acp::ToolKind::Other acp::ToolKind::Other
} }
fn initial_title(&self, _input: Self::Input) -> SharedString { fn initial_title(&self, _input: Result<Self::Input, serde_json::Value>) -> SharedString {
"List of random words".into() "List of random words".into()
} }

View file

@ -102,9 +102,8 @@ pub enum AgentResponseEvent {
Text(String), Text(String),
Thinking(String), Thinking(String),
ToolCall(acp::ToolCall), ToolCall(acp::ToolCall),
ToolCallUpdate(acp::ToolCallUpdate), ToolCallUpdate(acp_thread::ToolCallUpdate),
ToolCallAuthorization(ToolCallAuthorization), ToolCallAuthorization(ToolCallAuthorization),
ToolCallDiff(ToolCallDiff),
Stop(acp::StopReason), Stop(acp::StopReason),
} }
@ -115,12 +114,6 @@ pub struct ToolCallAuthorization {
pub response: oneshot::Sender<acp::PermissionOptionId>, pub response: oneshot::Sender<acp::PermissionOptionId>,
} }
#[derive(Debug)]
pub struct ToolCallDiff {
pub tool_call_id: acp::ToolCallId,
pub diff: Entity<acp_thread::Diff>,
}
pub struct Thread { pub struct Thread {
messages: Vec<AgentMessage>, messages: Vec<AgentMessage>,
completion_mode: CompletionMode, completion_mode: CompletionMode,
@ -294,7 +287,7 @@ impl Thread {
while let Some(tool_result) = tool_uses.next().await { while let Some(tool_result) = tool_uses.next().await {
log::info!("Tool finished {:?}", tool_result); log::info!("Tool finished {:?}", tool_result);
event_stream.send_tool_call_update( event_stream.update_tool_call_fields(
&tool_result.tool_use_id, &tool_result.tool_use_id,
acp::ToolCallUpdateFields { acp::ToolCallUpdateFields {
status: Some(if tool_result.is_error { status: Some(if tool_result.is_error {
@ -474,15 +467,24 @@ 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());
kind = tool.kind();
}
if push_new_tool_use { if push_new_tool_use {
event_stream.send_tool_call(tool.as_ref(), &tool_use); event_stream.send_tool_call(&tool_use.id, title, kind, tool_use.input.clone());
last_message last_message
.content .content
.push(MessageContent::ToolUse(tool_use.clone())); .push(MessageContent::ToolUse(tool_use.clone()));
} else { } else {
event_stream.send_tool_call_update( event_stream.update_tool_call_fields(
&tool_use.id, &tool_use.id,
acp::ToolCallUpdateFields { acp::ToolCallUpdateFields {
title: Some(title.into()),
kind: Some(kind),
raw_input: Some(tool_use.input.clone()), raw_input: Some(tool_use.input.clone()),
..Default::default() ..Default::default()
}, },
@ -506,7 +508,7 @@ impl Thread {
let tool_event_stream = let tool_event_stream =
ToolCallEventStream::new(&tool_use, tool.kind(), event_stream.clone()); ToolCallEventStream::new(&tool_use, tool.kind(), event_stream.clone());
tool_event_stream.send_update(acp::ToolCallUpdateFields { tool_event_stream.update_fields(acp::ToolCallUpdateFields {
status: Some(acp::ToolCallStatus::InProgress), status: Some(acp::ToolCallStatus::InProgress),
..Default::default() ..Default::default()
}); });
@ -693,7 +695,7 @@ where
fn kind(&self) -> acp::ToolKind; fn kind(&self) -> acp::ToolKind;
/// The initial tool title to display. Can be updated during the tool run. /// The initial tool title to display. Can be updated during the tool run.
fn initial_title(&self, input: Self::Input) -> SharedString; fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString;
/// Returns the JSON schema that describes the tool's input. /// Returns the JSON schema that describes the tool's input.
fn input_schema(&self) -> Schema { fn input_schema(&self) -> Schema {
@ -724,7 +726,7 @@ pub trait AnyAgentTool {
fn name(&self) -> SharedString; fn name(&self) -> SharedString;
fn description(&self, cx: &mut App) -> SharedString; fn description(&self, cx: &mut App) -> SharedString;
fn kind(&self) -> acp::ToolKind; fn kind(&self) -> acp::ToolKind;
fn initial_title(&self, input: serde_json::Value) -> Result<SharedString>; fn initial_title(&self, input: serde_json::Value) -> SharedString;
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value>; fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value>;
fn run( fn run(
self: Arc<Self>, self: Arc<Self>,
@ -750,9 +752,9 @@ where
self.0.kind() self.0.kind()
} }
fn initial_title(&self, input: serde_json::Value) -> Result<SharedString> { fn initial_title(&self, input: serde_json::Value) -> SharedString {
let parsed_input = serde_json::from_value(input)?; let parsed_input = serde_json::from_value(input.clone()).map_err(|_| input);
Ok(self.0.initial_title(parsed_input)) self.0.initial_title(parsed_input)
} }
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> { fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
@ -842,17 +844,17 @@ impl AgentResponseEventStream {
fn send_tool_call( fn send_tool_call(
&self, &self,
tool: Option<&Arc<dyn AnyAgentTool>>, id: &LanguageModelToolUseId,
tool_use: &LanguageModelToolUse, title: SharedString,
kind: acp::ToolKind,
input: serde_json::Value,
) { ) {
self.0 self.0
.unbounded_send(Ok(AgentResponseEvent::ToolCall(Self::initial_tool_call( .unbounded_send(Ok(AgentResponseEvent::ToolCall(Self::initial_tool_call(
&tool_use.id, id,
tool.and_then(|t| t.initial_title(tool_use.input.clone()).ok()) title.to_string(),
.map(|i| i.into()) kind,
.unwrap_or_else(|| tool_use.name.to_string()), input,
tool.map(|t| t.kind()).unwrap_or(acp::ToolKind::Other),
tool_use.input.clone(),
)))) ))))
.ok(); .ok();
} }
@ -875,7 +877,7 @@ impl AgentResponseEventStream {
} }
} }
fn send_tool_call_update( fn update_tool_call_fields(
&self, &self,
tool_use_id: &LanguageModelToolUseId, tool_use_id: &LanguageModelToolUseId,
fields: acp::ToolCallUpdateFields, fields: acp::ToolCallUpdateFields,
@ -885,14 +887,21 @@ impl AgentResponseEventStream {
acp::ToolCallUpdate { acp::ToolCallUpdate {
id: acp::ToolCallId(tool_use_id.to_string().into()), id: acp::ToolCallId(tool_use_id.to_string().into()),
fields, fields,
}, }
.into(),
))) )))
.ok(); .ok();
} }
fn send_tool_call_diff(&self, tool_call_diff: ToolCallDiff) { fn update_tool_call_diff(&self, tool_use_id: &LanguageModelToolUseId, diff: Entity<Diff>) {
self.0 self.0
.unbounded_send(Ok(AgentResponseEvent::ToolCallDiff(tool_call_diff))) .unbounded_send(Ok(AgentResponseEvent::ToolCallUpdate(
acp_thread::ToolCallUpdateDiff {
id: acp::ToolCallId(tool_use_id.to_string().into()),
diff,
}
.into(),
)))
.ok(); .ok();
} }
@ -964,15 +973,13 @@ impl ToolCallEventStream {
} }
} }
pub fn send_update(&self, fields: acp::ToolCallUpdateFields) { pub fn update_fields(&self, fields: acp::ToolCallUpdateFields) {
self.stream.send_tool_call_update(&self.tool_use_id, fields); self.stream
.update_tool_call_fields(&self.tool_use_id, fields);
} }
pub fn send_diff(&self, diff: Entity<Diff>) { pub fn update_diff(&self, diff: Entity<Diff>) {
self.stream.send_tool_call_diff(ToolCallDiff { self.stream.update_tool_call_diff(&self.tool_use_id, diff);
tool_call_id: acp::ToolCallId(self.tool_use_id.to_string().into()),
diff,
});
} }
pub fn authorize(&self, title: String) -> impl use<> + Future<Output = Result<()>> { pub fn authorize(&self, title: String) -> impl use<> + Future<Output = Result<()>> {

View file

@ -1,3 +1,4 @@
use crate::{AgentTool, Thread, ToolCallEventStream};
use acp_thread::Diff; use acp_thread::Diff;
use agent_client_protocol as acp; use agent_client_protocol as acp;
use anyhow::{anyhow, Context as _, Result}; use anyhow::{anyhow, Context as _, Result};
@ -20,7 +21,7 @@ use std::sync::Arc;
use ui::SharedString; use ui::SharedString;
use util::ResultExt; use util::ResultExt;
use crate::{AgentTool, Thread, ToolCallEventStream}; const DEFAULT_UI_TEXT: &str = "Editing file";
/// This is a tool for creating a new file or editing an existing file. For moving or renaming files, you should generally use the `terminal` tool with the 'mv' command instead. /// This is a tool for creating a new file or editing an existing file. For moving or renaming files, you should generally use the `terminal` tool with the 'mv' command instead.
/// ///
@ -78,6 +79,14 @@ pub struct EditFileToolInput {
pub mode: EditFileMode, pub mode: EditFileMode,
} }
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
struct EditFileToolPartialInput {
#[serde(default)]
path: String,
#[serde(default)]
display_description: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
pub enum EditFileMode { pub enum EditFileMode {
@ -182,8 +191,27 @@ impl AgentTool for EditFileTool {
acp::ToolKind::Edit acp::ToolKind::Edit
} }
fn initial_title(&self, input: Self::Input) -> SharedString { fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
input.display_description.into() match input {
Ok(input) => input.display_description.into(),
Err(raw_input) => {
if let Some(input) =
serde_json::from_value::<EditFileToolPartialInput>(raw_input).ok()
{
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()
}
}
} }
fn run( fn run(
@ -226,7 +254,7 @@ impl AgentTool for EditFileTool {
.await?; .await?;
let diff = cx.new(|cx| Diff::new(buffer.clone(), cx))?; let diff = cx.new(|cx| Diff::new(buffer.clone(), cx))?;
event_stream.send_diff(diff.clone()); event_stream.update_diff(diff.clone());
let old_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?; let old_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
let old_text = cx let old_text = cx
@ -1348,6 +1376,66 @@ mod tests {
} }
} }
#[gpui::test]
async fn test_initial_title_with_partial_input(cx: &mut TestAppContext) {
init_test(cx);
let fs = project::FakeFs::new(cx.executor());
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|_| {
Thread::new(
project.clone(),
Rc::default(),
action_log.clone(),
Templates::new(),
model.clone(),
)
});
let tool = Arc::new(EditFileTool { thread });
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
);
}
fn init_test(cx: &mut TestAppContext) { fn init_test(cx: &mut TestAppContext) {
cx.update(|cx| { cx.update(|cx| {
let settings_store = SettingsStore::test(cx); let settings_store = SettingsStore::test(cx);

View file

@ -94,8 +94,12 @@ impl AgentTool for FindPathTool {
acp::ToolKind::Search acp::ToolKind::Search
} }
fn initial_title(&self, input: Self::Input) -> SharedString { fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
format!("Find paths matching “`{}`”", input.glob).into() let mut title = "Find paths".to_string();
if let Ok(input) = input {
title.push_str(&format!(" matching “`{}`”", input.glob));
}
title.into()
} }
fn run( fn run(
@ -111,7 +115,7 @@ impl AgentTool for FindPathTool {
let paginated_matches: &[PathBuf] = &matches[cmp::min(input.offset, matches.len()) let paginated_matches: &[PathBuf] = &matches[cmp::min(input.offset, matches.len())
..cmp::min(input.offset + RESULTS_PER_PAGE, matches.len())]; ..cmp::min(input.offset + RESULTS_PER_PAGE, matches.len())];
event_stream.send_update(acp::ToolCallUpdateFields { event_stream.update_fields(acp::ToolCallUpdateFields {
title: Some(if paginated_matches.len() == 0 { title: Some(if paginated_matches.len() == 0 {
"No matches".into() "No matches".into()
} else if paginated_matches.len() == 1 { } else if paginated_matches.len() == 1 {

View file

@ -70,24 +70,28 @@ impl AgentTool for ReadFileTool {
acp::ToolKind::Read acp::ToolKind::Read
} }
fn initial_title(&self, input: Self::Input) -> SharedString { fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
let path = &input.path; if let Ok(input) = input {
match (input.start_line, input.end_line) { let path = &input.path;
(Some(start), Some(end)) => { match (input.start_line, input.end_line) {
format!( (Some(start), Some(end)) => {
"[Read file `{}` (lines {}-{})](@selection:{}:({}-{}))", format!(
path, start, end, path, start, end "[Read file `{}` (lines {}-{})](@selection:{}:({}-{}))",
) path, start, end, path, start, end
)
}
(Some(start), None) => {
format!(
"[Read file `{}` (from line {})](@selection:{}:({}-{}))",
path, start, path, start, start
)
}
_ => format!("[Read file `{}`](@file:{})", path, path),
} }
(Some(start), None) => { .into()
format!( } else {
"[Read file `{}` (from line {})](@selection:{}:({}-{}))", "Read file".into()
path, start, path, start, start
)
}
_ => format!("[Read file `{}`](@file:{})", path, path),
} }
.into()
} }
fn run( fn run(

View file

@ -30,7 +30,7 @@ impl AgentTool for ThinkingTool {
acp::ToolKind::Think acp::ToolKind::Think
} }
fn initial_title(&self, _input: Self::Input) -> SharedString { fn initial_title(&self, _input: Result<Self::Input, serde_json::Value>) -> SharedString {
"Thinking".into() "Thinking".into()
} }
@ -40,7 +40,7 @@ impl AgentTool for ThinkingTool {
event_stream: ToolCallEventStream, event_stream: ToolCallEventStream,
_cx: &mut App, _cx: &mut App,
) -> Task<Result<String>> { ) -> Task<Result<String>> {
event_stream.send_update(acp::ToolCallUpdateFields { event_stream.update_fields(acp::ToolCallUpdateFields {
content: Some(vec![input.content.into()]), content: Some(vec![input.content.into()]),
..Default::default() ..Default::default()
}); });