Update acp to 0.2.1 (#38068)

Release Notes:

- N/A
This commit is contained in:
Agus Zubiaga 2025-09-12 12:22:51 -03:00 committed by GitHub
parent 687c2c88c7
commit a577128163
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 191 additions and 64 deletions

4
Cargo.lock generated
View file

@ -196,9 +196,9 @@ dependencies = [
[[package]]
name = "agent-client-protocol"
version = "0.2.0-alpha.8"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08539e8d6b2ccca6cd00afdd42211698f7677adef09108a09414c11f1f45fdaf"
checksum = "003fb91bf1b8d6e15f72c45fb9171839af8241e81e3839fbb73536af113b7a79"
dependencies = [
"anyhow",
"async-broadcast",

View file

@ -434,7 +434,7 @@ zlog_settings = { path = "crates/zlog_settings" }
# External crates
#
agent-client-protocol = { version = "0.2.0-alpha.8", features = ["unstable"] }
agent-client-protocol = { version = "0.2.1", features = ["unstable"] }
aho-corasick = "1.1"
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" }
any_vec = "0.14"

View file

@ -862,7 +862,7 @@ impl AcpThread {
mut prompt_capabilities_rx: watch::Receiver<acp::PromptCapabilities>,
cx: &mut Context<Self>,
) -> Self {
let prompt_capabilities = *prompt_capabilities_rx.borrow();
let prompt_capabilities = prompt_capabilities_rx.borrow().clone();
let task = cx.spawn::<_, anyhow::Result<()>>(async move |this, cx| {
loop {
let caps = prompt_capabilities_rx.recv().await?;
@ -906,7 +906,7 @@ impl AcpThread {
}
pub fn prompt_capabilities(&self) -> acp::PromptCapabilities {
self.prompt_capabilities
self.prompt_capabilities.clone()
}
pub fn connection(&self) -> &Rc<dyn AgentConnection> {
@ -1446,6 +1446,7 @@ impl AcpThread {
vec![acp::ContentBlock::Text(acp::TextContent {
text: message.to_string(),
annotations: None,
meta: None,
})],
cx,
)
@ -1464,6 +1465,7 @@ impl AcpThread {
let request = acp::PromptRequest {
prompt: message.clone(),
session_id: self.session_id.clone(),
meta: None,
};
let git_store = self.project.read(cx).git_store().clone();
@ -1555,7 +1557,8 @@ impl AcpThread {
let canceled = matches!(
result,
Ok(Ok(acp::PromptResponse {
stop_reason: acp::StopReason::Cancelled
stop_reason: acp::StopReason::Cancelled,
meta: None,
}))
);
@ -1571,6 +1574,7 @@ impl AcpThread {
// Handle refusal - distinguish between user prompt and tool call refusals
if let Ok(Ok(acp::PromptResponse {
stop_reason: acp::StopReason::Refusal,
meta: _,
})) = result
{
if let Some((user_msg_ix, _)) = this.last_user_message() {
@ -2163,6 +2167,7 @@ mod tests {
acp::ContentBlock::Text(acp::TextContent {
annotations: None,
text: "Hello, ".to_string(),
meta: None,
}),
cx,
);
@ -2186,6 +2191,7 @@ mod tests {
acp::ContentBlock::Text(acp::TextContent {
annotations: None,
text: "world!".to_string(),
meta: None,
}),
cx,
);
@ -2207,6 +2213,7 @@ mod tests {
acp::ContentBlock::Text(acp::TextContent {
annotations: None,
text: "Assistant response".to_string(),
meta: None,
}),
false,
cx,
@ -2220,6 +2227,7 @@ mod tests {
acp::ContentBlock::Text(acp::TextContent {
annotations: None,
text: "New user message".to_string(),
meta: None,
}),
cx,
);
@ -2265,6 +2273,7 @@ mod tests {
})?;
Ok(acp::PromptResponse {
stop_reason: acp::StopReason::EndTurn,
meta: None,
})
}
.boxed_local()
@ -2335,6 +2344,7 @@ mod tests {
.unwrap();
Ok(acp::PromptResponse {
stop_reason: acp::StopReason::EndTurn,
meta: None,
})
}
.boxed_local()
@ -2403,6 +2413,7 @@ mod tests {
locations: vec![],
raw_input: None,
raw_output: None,
meta: None,
}),
cx,
)
@ -2411,6 +2422,7 @@ mod tests {
.unwrap();
Ok(acp::PromptResponse {
stop_reason: acp::StopReason::EndTurn,
meta: None,
})
}
.boxed_local()
@ -2459,6 +2471,7 @@ mod tests {
status: Some(acp::ToolCallStatus::Completed),
..Default::default()
},
meta: None,
}),
cx,
)
@ -2501,11 +2514,13 @@ mod tests {
path: "/test/test.txt".into(),
old_text: None,
new_text: "foo".into(),
meta: None,
},
}],
locations: vec![],
raw_input: None,
raw_output: None,
meta: None,
}),
cx,
)
@ -2514,6 +2529,7 @@ mod tests {
.unwrap();
Ok(acp::PromptResponse {
stop_reason: acp::StopReason::EndTurn,
meta: None,
})
}
.boxed_local()
@ -2576,6 +2592,7 @@ mod tests {
})?;
Ok(acp::PromptResponse {
stop_reason: acp::StopReason::EndTurn,
meta: None,
})
}
.boxed_local()
@ -2743,6 +2760,7 @@ mod tests {
raw_output: Some(
serde_json::json!({"result": "inappropriate content"}),
),
meta: None,
}),
cx,
)
@ -2752,10 +2770,12 @@ mod tests {
// Now return refusal because of the tool result
Ok(acp::PromptResponse {
stop_reason: acp::StopReason::Refusal,
meta: None,
})
} else {
Ok(acp::PromptResponse {
stop_reason: acp::StopReason::EndTurn,
meta: None,
})
}
}
@ -2789,6 +2809,7 @@ mod tests {
vec![acp::ContentBlock::Text(acp::TextContent {
text: "Hello".into(),
annotations: None,
meta: None,
})],
cx,
)
@ -2841,6 +2862,7 @@ mod tests {
async move {
Ok(acp::PromptResponse {
stop_reason: acp::StopReason::Refusal,
meta: None,
})
}
.boxed_local()
@ -2848,6 +2870,7 @@ mod tests {
async move {
Ok(acp::PromptResponse {
stop_reason: acp::StopReason::EndTurn,
meta: None,
})
}
.boxed_local()
@ -2909,6 +2932,7 @@ mod tests {
if refuse_next.load(SeqCst) {
return Ok(acp::PromptResponse {
stop_reason: acp::StopReason::Refusal,
meta: None,
});
}
@ -2927,6 +2951,7 @@ mod tests {
})?;
Ok(acp::PromptResponse {
stop_reason: acp::StopReason::EndTurn,
meta: None,
})
}
.boxed_local()
@ -3082,6 +3107,7 @@ mod tests {
image: true,
audio: true,
embedded_context: true,
meta: None,
}),
cx,
)
@ -3113,6 +3139,7 @@ mod tests {
} else {
Task::ready(Ok(acp::PromptResponse {
stop_reason: acp::StopReason::EndTurn,
meta: None,
}))
}
}

View file

@ -354,6 +354,7 @@ mod test_support {
image: true,
audio: true,
embedded_context: true,
meta: None,
}),
cx,
)
@ -393,7 +394,10 @@ mod test_support {
response_tx.replace(tx);
cx.spawn(async move |_| {
let stop_reason = rx.await?;
Ok(acp::PromptResponse { stop_reason })
Ok(acp::PromptResponse {
stop_reason,
meta: None,
})
})
} else {
for update in self.next_prompt_updates.lock().drain(..) {
@ -432,6 +436,7 @@ mod test_support {
try_join_all(tasks).await?;
Ok(acp::PromptResponse {
stop_reason: acp::StopReason::EndTurn,
meta: None,
})
})
}

View file

@ -75,6 +75,7 @@ impl Terminal {
acp::TerminalExitStatus {
exit_code: exit_status.as_ref().map(|e| e.exit_code()),
signal: exit_status.and_then(|e| e.signal().map(Into::into)),
meta: None,
}
})
.shared(),
@ -105,7 +106,9 @@ impl Terminal {
exit_status: Some(acp::TerminalExitStatus {
exit_code: exit_status.as_ref().map(|e| e.exit_code()),
signal: exit_status.and_then(|e| e.signal().map(Into::into)),
meta: None,
}),
meta: None,
}
} else {
let (current_content, original_len) = self.truncated_output(cx);
@ -114,6 +117,7 @@ impl Terminal {
truncated: current_content.len() < original_len,
output: current_content,
exit_status: None,
meta: None,
}
}
}

View file

@ -747,6 +747,7 @@ impl NativeAgentConnection {
acp::ContentBlock::Text(acp::TextContent {
text,
annotations: None,
meta: None,
}),
false,
cx,
@ -759,6 +760,7 @@ impl NativeAgentConnection {
acp::ContentBlock::Text(acp::TextContent {
text,
annotations: None,
meta: None,
}),
true,
cx,
@ -804,7 +806,10 @@ impl NativeAgentConnection {
}
ThreadEvent::Stop(stop_reason) => {
log::debug!("Assistant message complete: {:?}", stop_reason);
return Ok(acp::PromptResponse { stop_reason });
return Ok(acp::PromptResponse {
stop_reason,
meta: None,
});
}
}
}
@ -818,6 +823,7 @@ impl NativeAgentConnection {
log::debug!("Response stream completed");
anyhow::Ok(acp::PromptResponse {
stop_reason: acp::StopReason::EndTurn,
meta: None,
})
})
}
@ -1441,6 +1447,7 @@ mod tests {
mime_type: None,
size: None,
title: None,
meta: None,
}),
" mean?".into(),
],

View file

@ -1299,6 +1299,7 @@ async fn test_cancellation(cx: &mut TestAppContext) {
status: Some(acp::ToolCallStatus::Completed),
..
},
meta: None,
},
)) if Some(&id) == echo_id.as_ref() => {
echo_completed = true;
@ -1926,6 +1927,7 @@ async fn test_agent_connection(cx: &mut TestAppContext) {
acp::PromptRequest {
session_id: session_id.clone(),
prompt: vec!["ghi".into()],
meta: None,
},
cx,
)
@ -1990,6 +1992,7 @@ async fn test_tool_updates_to_completion(cx: &mut TestAppContext) {
locations: vec![],
raw_input: Some(json!({})),
raw_output: None,
meta: None,
}
);
let update = expect_tool_call_update_fields(&mut events).await;
@ -2003,6 +2006,7 @@ async fn test_tool_updates_to_completion(cx: &mut TestAppContext) {
raw_input: Some(json!({ "content": "Thinking hard!" })),
..Default::default()
},
meta: None,
}
);
let update = expect_tool_call_update_fields(&mut events).await;
@ -2014,6 +2018,7 @@ async fn test_tool_updates_to_completion(cx: &mut TestAppContext) {
status: Some(acp::ToolCallStatus::InProgress),
..Default::default()
},
meta: None,
}
);
let update = expect_tool_call_update_fields(&mut events).await;
@ -2025,6 +2030,7 @@ async fn test_tool_updates_to_completion(cx: &mut TestAppContext) {
content: Some(vec!["Thinking hard!".into()]),
..Default::default()
},
meta: None,
}
);
let update = expect_tool_call_update_fields(&mut events).await;
@ -2037,6 +2043,7 @@ async fn test_tool_updates_to_completion(cx: &mut TestAppContext) {
raw_output: Some("Finished thinking.".into()),
..Default::default()
},
meta: None,
}
);
}

View file

@ -614,6 +614,7 @@ impl Thread {
fn prompt_capabilities(model: Option<&dyn LanguageModel>) -> acp::PromptCapabilities {
let image = model.map_or(true, |model| model.supports_images());
acp::PromptCapabilities {
meta: None,
image,
audio: false,
embedded_context: true,
@ -728,6 +729,7 @@ impl Thread {
stream
.0
.unbounded_send(Ok(ThreadEvent::ToolCall(acp::ToolCall {
meta: None,
id: acp::ToolCallId(tool_use.id.to_string().into()),
title: tool_use.name.to_string(),
kind: acp::ToolKind::Other,
@ -2333,6 +2335,7 @@ impl ThreadEventStream {
input: serde_json::Value,
) -> acp::ToolCall {
acp::ToolCall {
meta: None,
id: acp::ToolCallId(id.to_string().into()),
title,
kind,
@ -2352,6 +2355,7 @@ impl ThreadEventStream {
self.0
.unbounded_send(Ok(ThreadEvent::ToolCallUpdate(
acp::ToolCallUpdate {
meta: None,
id: acp::ToolCallId(tool_use_id.to_string().into()),
fields,
}
@ -2437,6 +2441,7 @@ impl ToolCallEventStream {
.unbounded_send(Ok(ThreadEvent::ToolCallAuthorization(
ToolCallAuthorization {
tool_call: acp::ToolCallUpdate {
meta: None,
id: acp::ToolCallId(self.tool_use_id.to_string().into()),
fields: acp::ToolCallUpdateFields {
title: Some(title.into()),
@ -2448,16 +2453,19 @@ impl ToolCallEventStream {
id: acp::PermissionOptionId("always_allow".into()),
name: "Always Allow".into(),
kind: acp::PermissionOptionKind::AllowAlways,
meta: None,
},
acp::PermissionOption {
id: acp::PermissionOptionId("allow".into()),
name: "Allow".into(),
kind: acp::PermissionOptionKind::AllowOnce,
meta: None,
},
acp::PermissionOption {
id: acp::PermissionOptionId("deny".into()),
name: "Deny".into(),
kind: acp::PermissionOptionKind::RejectOnce,
meta: None,
},
],
response: response_tx,
@ -2611,17 +2619,21 @@ impl From<UserMessageContent> for acp::ContentBlock {
UserMessageContent::Text(text) => acp::ContentBlock::Text(acp::TextContent {
text,
annotations: None,
meta: None,
}),
UserMessageContent::Image(image) => acp::ContentBlock::Image(acp::ImageContent {
data: image.source.to_string(),
mime_type: "image/png".to_string(),
meta: None,
annotations: None,
uri: None,
}),
UserMessageContent::Mention { uri, content } => {
acp::ContentBlock::Resource(acp::EmbeddedResource {
meta: None,
resource: acp::EmbeddedResourceResource::TextResourceContents(
acp::TextResourceContents {
meta: None,
mime_type: None,
text: content,
uri: uri.to_uri().to_string(),

View file

@ -274,6 +274,7 @@ impl AgentTool for EditFileTool {
locations: Some(vec![acp::ToolCallLocation {
path: abs_path,
line: None,
meta: None,
}]),
..Default::default()
});
@ -353,7 +354,7 @@ impl AgentTool for EditFileTool {
}).ok();
if let Some(abs_path) = abs_path.clone() {
event_stream.update_fields(ToolCallUpdateFields {
locations: Some(vec![ToolCallLocation { path: abs_path, line }]),
locations: Some(vec![ToolCallLocation { path: abs_path, line, meta: None }]),
..Default::default()
});
}

View file

@ -138,6 +138,7 @@ impl AgentTool for FindPathTool {
mime_type: None,
size: None,
title: None,
meta: None,
}),
})
.collect(),

View file

@ -149,6 +149,7 @@ impl AgentTool for ReadFileTool {
locations: Some(vec![acp::ToolCallLocation {
path: abs_path.clone(),
line: input.start_line.map(|line| line.saturating_sub(1)),
meta: None,
}]),
..Default::default()
});

View file

@ -122,6 +122,7 @@ fn emit_update(response: &WebSearchResponse, event_stream: &ToolCallEventStream)
mime_type: None,
annotations: None,
size: None,
meta: None,
}),
})
.collect(),

View file

@ -13,7 +13,7 @@ use util::ResultExt as _;
use std::path::PathBuf;
use std::{any::Any, cell::RefCell};
use std::{path::Path, rc::Rc};
use std::{path::Path, rc::Rc, sync::Arc};
use thiserror::Error;
use anyhow::{Context as _, Result};
@ -156,9 +156,12 @@ impl AcpConnection {
fs: acp::FileSystemCapability {
read_text_file: true,
write_text_file: true,
meta: None,
},
terminal: true,
meta: None,
},
meta: None,
})
.await?;
@ -226,6 +229,7 @@ impl AgentConnection for AcpConnection {
.map(|(name, value)| acp::EnvVariable {
name: name.clone(),
value: value.clone(),
meta: None,
})
.collect()
} else {
@ -243,7 +247,7 @@ impl AgentConnection for AcpConnection {
cx.spawn(async move |cx| {
let response = conn
.new_session(acp::NewSessionRequest { mcp_servers, cwd })
.new_session(acp::NewSessionRequest { mcp_servers, cwd, meta: None })
.await
.map_err(|err| {
if err.code == acp::ErrorCode::AUTH_REQUIRED.code {
@ -277,6 +281,7 @@ impl AgentConnection for AcpConnection {
let result = conn.set_session_mode(acp::SetSessionModeRequest {
session_id,
mode_id: default_mode,
meta: None,
})
.await.log_err();
@ -316,7 +321,7 @@ impl AgentConnection for AcpConnection {
action_log,
session_id.clone(),
// ACP doesn't currently support per-session prompt capabilities or changing capabilities dynamically.
watch::Receiver::constant(self.agent_capabilities.prompt_capabilities),
watch::Receiver::constant(self.agent_capabilities.prompt_capabilities.clone()),
cx,
)
})?;
@ -339,13 +344,13 @@ impl AgentConnection for AcpConnection {
fn authenticate(&self, method_id: acp::AuthMethodId, cx: &mut App) -> Task<Result<()>> {
let conn = self.connection.clone();
cx.foreground_executor().spawn(async move {
let result = conn
.authenticate(acp::AuthenticateRequest {
method_id: method_id.clone(),
})
.await?;
conn.authenticate(acp::AuthenticateRequest {
method_id: method_id.clone(),
meta: None,
})
.await?;
Ok(result)
Ok(())
})
}
@ -396,6 +401,7 @@ impl AgentConnection for AcpConnection {
{
Ok(acp::PromptResponse {
stop_reason: acp::StopReason::Cancelled,
meta: None,
})
} else {
Err(anyhow!(details))
@ -415,6 +421,7 @@ impl AgentConnection for AcpConnection {
let conn = self.connection.clone();
let params = acp::CancelNotification {
session_id: session_id.clone(),
meta: None,
};
cx.foreground_executor()
.spawn(async move { conn.cancel(params).await })
@ -478,6 +485,7 @@ impl acp_thread::AgentSessionModes for AcpSessionModes {
.set_session_mode(acp::SetSessionModeRequest {
session_id,
mode_id,
meta: None,
})
.await;
@ -526,13 +534,16 @@ impl acp::Client for ClientDelegate {
let outcome = task.await;
Ok(acp::RequestPermissionResponse { outcome })
Ok(acp::RequestPermissionResponse {
outcome,
meta: None,
})
}
async fn write_text_file(
&self,
arguments: acp::WriteTextFileRequest,
) -> Result<(), acp::Error> {
) -> Result<acp::WriteTextFileResponse, acp::Error> {
let cx = &mut self.cx.clone();
let task = self
.session_thread(&arguments.session_id)?
@ -542,7 +553,7 @@ impl acp::Client for ClientDelegate {
task.await?;
Ok(())
Ok(Default::default())
}
async fn read_text_file(
@ -558,7 +569,10 @@ impl acp::Client for ClientDelegate {
let content = task.await?;
Ok(acp::ReadTextFileResponse { content })
Ok(acp::ReadTextFileResponse {
content,
meta: None,
})
}
async fn session_notification(
@ -607,26 +621,49 @@ impl acp::Client for ClientDelegate {
Ok(
terminal.read_with(&self.cx, |terminal, _| acp::CreateTerminalResponse {
terminal_id: terminal.id().clone(),
meta: None,
})?,
)
}
async fn kill_terminal(&self, args: acp::KillTerminalRequest) -> Result<(), acp::Error> {
async fn kill_terminal_command(
&self,
args: acp::KillTerminalCommandRequest,
) -> Result<acp::KillTerminalCommandResponse, acp::Error> {
self.session_thread(&args.session_id)?
.update(&mut self.cx.clone(), |thread, cx| {
thread.kill_terminal(args.terminal_id, cx)
})??;
Ok(())
Ok(Default::default())
}
async fn release_terminal(&self, args: acp::ReleaseTerminalRequest) -> Result<(), acp::Error> {
async fn ext_method(
&self,
_name: Arc<str>,
_params: Arc<serde_json::value::RawValue>,
) -> Result<Arc<serde_json::value::RawValue>, acp::Error> {
Err(acp::Error::method_not_found())
}
async fn ext_notification(
&self,
_name: Arc<str>,
_params: Arc<serde_json::value::RawValue>,
) -> Result<(), acp::Error> {
Err(acp::Error::method_not_found())
}
async fn release_terminal(
&self,
args: acp::ReleaseTerminalRequest,
) -> Result<acp::ReleaseTerminalResponse, acp::Error> {
self.session_thread(&args.session_id)?
.update(&mut self.cx.clone(), |thread, cx| {
thread.release_terminal(args.terminal_id, cx)
})??;
Ok(())
Ok(Default::default())
}
async fn terminal_output(
@ -655,7 +692,10 @@ impl acp::Client for ClientDelegate {
})??
.await;
Ok(acp::WaitForTerminalExitResponse { exit_status })
Ok(acp::WaitForTerminalExitResponse {
exit_status,
meta: None,
})
}
}

View file

@ -83,6 +83,7 @@ where
acp::ContentBlock::Text(acp::TextContent {
text: "Read the file ".into(),
annotations: None,
meta: None,
}),
acp::ContentBlock::ResourceLink(acp::ResourceLink {
uri: "foo.rs".into(),
@ -92,10 +93,12 @@ where
mime_type: None,
size: None,
title: None,
meta: None,
}),
acp::ContentBlock::Text(acp::TextContent {
text: " and tell me what the content of the println! is".into(),
annotations: None,
meta: None,
}),
],
cx,

View file

@ -1,4 +1,4 @@
use std::cell::{Cell, RefCell};
use std::cell::RefCell;
use std::ops::Range;
use std::rc::Rc;
use std::sync::Arc;
@ -68,7 +68,7 @@ pub struct ContextPickerCompletionProvider {
workspace: WeakEntity<Workspace>,
history_store: Entity<HistoryStore>,
prompt_store: Option<Entity<PromptStore>>,
prompt_capabilities: Rc<Cell<acp::PromptCapabilities>>,
prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
}
@ -78,7 +78,7 @@ impl ContextPickerCompletionProvider {
workspace: WeakEntity<Workspace>,
history_store: Entity<HistoryStore>,
prompt_store: Option<Entity<PromptStore>>,
prompt_capabilities: Rc<Cell<acp::PromptCapabilities>>,
prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
) -> Self {
Self {
@ -600,7 +600,7 @@ impl ContextPickerCompletionProvider {
}),
);
if self.prompt_capabilities.get().embedded_context {
if self.prompt_capabilities.borrow().embedded_context {
const RECENT_COUNT: usize = 2;
let threads = self
.history_store
@ -622,7 +622,7 @@ impl ContextPickerCompletionProvider {
workspace: &Entity<Workspace>,
cx: &mut App,
) -> Vec<ContextPickerEntry> {
let embedded_context = self.prompt_capabilities.get().embedded_context;
let embedded_context = self.prompt_capabilities.borrow().embedded_context;
let mut entries = if embedded_context {
vec![
ContextPickerEntry::Mode(ContextPickerMode::File),
@ -694,7 +694,7 @@ impl CompletionProvider for ContextPickerCompletionProvider {
ContextCompletion::try_parse(
line,
offset_to_line,
self.prompt_capabilities.get().embedded_context,
self.prompt_capabilities.borrow().embedded_context,
)
});
let Some(state) = state else {
@ -896,7 +896,7 @@ impl CompletionProvider for ContextPickerCompletionProvider {
ContextCompletion::try_parse(
line,
offset_to_line,
self.prompt_capabilities.get().embedded_context,
self.prompt_capabilities.borrow().embedded_context,
)
.map(|completion| {
completion.source_range().start <= offset_to_line + position.column as usize

View file

@ -1,8 +1,4 @@
use std::{
cell::{Cell, RefCell},
ops::Range,
rc::Rc,
};
use std::{cell::RefCell, ops::Range, rc::Rc};
use acp_thread::{AcpThread, AgentThreadEntry};
use agent_client_protocol::{self as acp, ToolCallId};
@ -30,7 +26,7 @@ pub struct EntryViewState {
history_store: Entity<HistoryStore>,
prompt_store: Option<Entity<PromptStore>>,
entries: Vec<Entry>,
prompt_capabilities: Rc<Cell<acp::PromptCapabilities>>,
prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
agent_name: SharedString,
}
@ -41,7 +37,7 @@ impl EntryViewState {
project: Entity<Project>,
history_store: Entity<HistoryStore>,
prompt_store: Option<Entity<PromptStore>>,
prompt_capabilities: Rc<Cell<acp::PromptCapabilities>>,
prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
agent_name: SharedString,
) -> Self {
@ -448,11 +444,13 @@ mod tests {
path: "/project/hello.txt".into(),
old_text: Some("hi world".into()),
new_text: "hello world".into(),
meta: None,
},
}],
locations: vec![],
raw_input: None,
raw_output: None,
meta: None,
};
let connection = Rc::new(StubAgentConnection::new());
let thread = cx

View file

@ -36,7 +36,7 @@ use prompt_store::{PromptId, PromptStore};
use rope::Point;
use settings::Settings;
use std::{
cell::{Cell, RefCell},
cell::RefCell,
ffi::OsStr,
fmt::Write,
ops::{Range, RangeInclusive},
@ -64,7 +64,7 @@ pub struct MessageEditor {
workspace: WeakEntity<Workspace>,
history_store: Entity<HistoryStore>,
prompt_store: Option<Entity<PromptStore>>,
prompt_capabilities: Rc<Cell<acp::PromptCapabilities>>,
prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
agent_name: SharedString,
_subscriptions: Vec<Subscription>,
@ -89,7 +89,7 @@ impl MessageEditor {
project: Entity<Project>,
history_store: Entity<HistoryStore>,
prompt_store: Option<Entity<PromptStore>>,
prompt_capabilities: Rc<Cell<acp::PromptCapabilities>>,
prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
agent_name: SharedString,
placeholder: &str,
@ -428,7 +428,7 @@ impl MessageEditor {
.unwrap_or_default();
if Img::extensions().contains(&extension) && !extension.contains("svg") {
if !self.prompt_capabilities.get().image {
if !self.prompt_capabilities.borrow().image {
return Task::ready(Err(anyhow!("This model does not support images yet")));
}
let task = self
@ -789,7 +789,7 @@ impl MessageEditor {
let contents = self
.mention_set
.contents(&self.prompt_capabilities.get(), cx);
.contents(&self.prompt_capabilities.borrow(), cx);
let editor = self.editor.clone();
cx.spawn(async move |_, cx| {
@ -834,8 +834,10 @@ impl MessageEditor {
mime_type: None,
text: content.clone(),
uri: uri.to_uri().to_string(),
meta: None,
},
),
meta: None,
})
}
Mention::Image(mention_image) => {
@ -855,6 +857,7 @@ impl MessageEditor {
data: mention_image.data.to_string(),
mime_type: mention_image.format.mime_type().into(),
uri,
meta: None,
})
}
Mention::UriOnly => {
@ -866,6 +869,7 @@ impl MessageEditor {
mime_type: None,
size: None,
title: None,
meta: None,
})
}
};
@ -920,7 +924,7 @@ impl MessageEditor {
}
fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
if !self.prompt_capabilities.get().image {
if !self.prompt_capabilities.borrow().image {
return;
}
@ -1188,6 +1192,7 @@ impl MessageEditor {
data,
mime_type,
annotations: _,
meta: _,
}) => {
let mention_uri = if let Some(uri) = uri {
MentionUri::parse(&uri)
@ -1571,13 +1576,7 @@ impl Addon for MessageEditorAddon {
#[cfg(test)]
mod tests {
use std::{
cell::{Cell, RefCell},
ops::Range,
path::Path,
rc::Rc,
sync::Arc,
};
use std::{cell::RefCell, ops::Range, path::Path, rc::Rc, sync::Arc};
use acp_thread::MentionUri;
use agent_client_protocol as acp;
@ -1724,7 +1723,7 @@ mod tests {
let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
let context_store = cx.new(|cx| ContextStore::fake(project.clone(), cx));
let history_store = cx.new(|cx| HistoryStore::new(context_store, cx));
let prompt_capabilities = Rc::new(Cell::new(acp::PromptCapabilities::default()));
let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
// Start with no available commands - simulating Claude which doesn't support slash commands
let available_commands = Rc::new(RefCell::new(vec![]));
@ -1773,6 +1772,7 @@ mod tests {
name: "help".to_string(),
description: "Get help".to_string(),
input: None,
meta: None,
}]);
// Test that unsupported slash commands trigger an error when we have a list of available commands
@ -1887,12 +1887,13 @@ mod tests {
let context_store = cx.new(|cx| ContextStore::fake(project.clone(), cx));
let history_store = cx.new(|cx| HistoryStore::new(context_store, cx));
let prompt_capabilities = Rc::new(Cell::new(acp::PromptCapabilities::default()));
let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
let available_commands = Rc::new(RefCell::new(vec![
acp::AvailableCommand {
name: "quick-math".to_string(),
description: "2 + 2 = 4 - 1 = 3".to_string(),
input: None,
meta: None,
},
acp::AvailableCommand {
name: "say-hello".to_string(),
@ -1900,6 +1901,7 @@ mod tests {
input: Some(acp::AvailableCommandInput::Unstructured {
hint: "<name>".to_string(),
}),
meta: None,
},
]));
@ -2134,7 +2136,7 @@ mod tests {
let context_store = cx.new(|cx| ContextStore::fake(project.clone(), cx));
let history_store = cx.new(|cx| HistoryStore::new(context_store, cx));
let prompt_capabilities = Rc::new(Cell::new(acp::PromptCapabilities::default()));
let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
let (message_editor, editor) = workspace.update_in(&mut cx, |workspace, window, cx| {
let workspace_handle = cx.weak_entity();
@ -2189,10 +2191,11 @@ mod tests {
editor.set_text("", window, cx);
});
prompt_capabilities.set(acp::PromptCapabilities {
prompt_capabilities.replace(acp::PromptCapabilities {
image: true,
audio: true,
embedded_context: true,
meta: None,
});
cx.simulate_input("Lorem ");
@ -2264,6 +2267,7 @@ mod tests {
image: true,
audio: true,
embedded_context: true,
meta: None,
};
let contents = message_editor
@ -2640,8 +2644,9 @@ mod tests {
cx,
);
// Enable embedded context so files are actually included
editor.prompt_capabilities.set(acp::PromptCapabilities {
editor.prompt_capabilities.replace(acp::PromptCapabilities {
embedded_context: true,
meta: None,
..Default::default()
});
editor

View file

@ -37,7 +37,7 @@ use project::{Project, ProjectEntryId};
use prompt_store::{PromptId, PromptStore};
use rope::Point;
use settings::{Settings as _, SettingsStore};
use std::cell::{Cell, RefCell};
use std::cell::RefCell;
use std::path::Path;
use std::sync::Arc;
use std::time::Instant;
@ -290,7 +290,7 @@ pub struct AcpThreadView {
editor_expanded: bool,
should_be_following: bool,
editing_message: Option<usize>,
prompt_capabilities: Rc<Cell<PromptCapabilities>>,
prompt_capabilities: Rc<RefCell<PromptCapabilities>>,
available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
is_loading_contents: bool,
new_server_version_available: Option<SharedString>,
@ -334,7 +334,7 @@ impl AcpThreadView {
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let prompt_capabilities = Rc::new(Cell::new(acp::PromptCapabilities::default()));
let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
let available_commands = Rc::new(RefCell::new(vec![]));
let placeholder = if agent.name() == "Zed Agent" {
@ -559,7 +559,7 @@ impl AcpThreadView {
let action_log = thread.read(cx).action_log().clone();
this.prompt_capabilities
.set(thread.read(cx).prompt_capabilities());
.replace(thread.read(cx).prompt_capabilities());
let count = thread.read(cx).entries().len();
this.entry_view_state.update(cx, |view_state, cx| {
@ -1373,7 +1373,7 @@ impl AcpThreadView {
}
AcpThreadEvent::PromptCapabilitiesUpdated => {
self.prompt_capabilities
.set(thread.read(cx).prompt_capabilities());
.replace(thread.read(cx).prompt_capabilities());
}
AcpThreadEvent::TokenUsageUpdated => {}
AcpThreadEvent::AvailableCommandsUpdated(available_commands) => {
@ -1390,11 +1390,13 @@ impl AcpThreadView {
name: "login".to_owned(),
description: "Authenticate".to_owned(),
input: None,
meta: None,
});
available_commands.push(acp::AvailableCommand {
name: "logout".to_owned(),
description: "Authenticate".to_owned(),
input: None,
meta: None,
});
}
@ -5722,6 +5724,7 @@ pub(crate) mod tests {
locations: vec![],
raw_input: None,
raw_output: None,
meta: None,
};
let connection =
StubAgentConnection::new().with_permission_requests(HashMap::from_iter([(
@ -5730,6 +5733,7 @@ pub(crate) mod tests {
id: acp::PermissionOptionId("1".into()),
name: "Allow".into(),
kind: acp::PermissionOptionKind::AllowOnce,
meta: None,
}],
)]));
@ -5906,6 +5910,7 @@ pub(crate) mod tests {
image: true,
audio: true,
embedded_context: true,
meta: None,
}),
cx,
)
@ -5965,6 +5970,7 @@ pub(crate) mod tests {
image: true,
audio: true,
embedded_context: true,
meta: None,
}),
cx,
)
@ -5991,6 +5997,7 @@ pub(crate) mod tests {
) -> Task<gpui::Result<acp::PromptResponse>> {
Task::ready(Ok(acp::PromptResponse {
stop_reason: acp::StopReason::Refusal,
meta: None,
}))
}
@ -6074,11 +6081,13 @@ pub(crate) mod tests {
path: "/project/test1.txt".into(),
old_text: Some("old content 1".into()),
new_text: "new content 1".into(),
meta: None,
},
}],
locations: vec![],
raw_input: None,
raw_output: None,
meta: None,
})]);
thread
@ -6115,11 +6124,13 @@ pub(crate) mod tests {
path: "/project/test2.txt".into(),
old_text: Some("old content 2".into()),
new_text: "new content 2".into(),
meta: None,
},
}],
locations: vec![],
raw_input: None,
raw_output: None,
meta: None,
})]);
thread
@ -6197,6 +6208,7 @@ pub(crate) mod tests {
content: acp::ContentBlock::Text(acp::TextContent {
text: "Response".into(),
annotations: None,
meta: None,
}),
}]);
@ -6286,6 +6298,7 @@ pub(crate) mod tests {
content: acp::ContentBlock::Text(acp::TextContent {
text: "Response".into(),
annotations: None,
meta: None,
}),
}]);
@ -6329,6 +6342,7 @@ pub(crate) mod tests {
content: acp::ContentBlock::Text(acp::TextContent {
text: "New Response".into(),
annotations: None,
meta: None,
}),
}]);
@ -6421,6 +6435,7 @@ pub(crate) mod tests {
content: acp::ContentBlock::Text(acp::TextContent {
text: "Response".into(),
annotations: None,
meta: None,
}),
},
cx,