claude: Include all mentions and images in user message (#36539)
User messages sent to Claude Code will now include the content of all mentions, and any images included. Release Notes: - N/A
This commit is contained in:
parent
ce216432be
commit
714c36fa7b
1 changed files with 218 additions and 24 deletions
|
@ -32,7 +32,7 @@ use util::{ResultExt, debug_panic};
|
|||
use crate::claude::mcp_server::{ClaudeZedMcpServer, McpConfig};
|
||||
use crate::claude::tools::ClaudeTool;
|
||||
use crate::{AgentServer, AgentServerCommand, AllAgentServersSettings};
|
||||
use acp_thread::{AcpThread, AgentConnection, AuthRequired};
|
||||
use acp_thread::{AcpThread, AgentConnection, AuthRequired, MentionUri};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ClaudeCode;
|
||||
|
@ -267,27 +267,12 @@ impl AgentConnection for ClaudeAgentConnection {
|
|||
let (end_tx, end_rx) = oneshot::channel();
|
||||
session.turn_state.replace(TurnState::InProgress { end_tx });
|
||||
|
||||
let mut content = String::new();
|
||||
for chunk in params.prompt {
|
||||
match chunk {
|
||||
acp::ContentBlock::Text(text_content) => {
|
||||
content.push_str(&text_content.text);
|
||||
}
|
||||
acp::ContentBlock::ResourceLink(resource_link) => {
|
||||
content.push_str(&format!("@{}", resource_link.uri));
|
||||
}
|
||||
acp::ContentBlock::Audio(_)
|
||||
| acp::ContentBlock::Image(_)
|
||||
| acp::ContentBlock::Resource(_) => {
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
}
|
||||
let content = acp_content_to_claude(params.prompt);
|
||||
|
||||
if let Err(err) = session.outgoing_tx.unbounded_send(SdkMessage::User {
|
||||
message: Message {
|
||||
role: Role::User,
|
||||
content: Content::UntaggedText(content),
|
||||
content: Content::Chunks(content),
|
||||
id: None,
|
||||
model: None,
|
||||
stop_reason: None,
|
||||
|
@ -513,10 +498,17 @@ impl ClaudeAgentSession {
|
|||
chunk
|
||||
);
|
||||
}
|
||||
ContentChunk::Image { source } => {
|
||||
if !turn_state.borrow().is_canceled() {
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.push_user_content_block(None, source.into(), cx)
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
|
||||
ContentChunk::Image
|
||||
| ContentChunk::Document
|
||||
| ContentChunk::WebSearchToolResult => {
|
||||
ContentChunk::Document | ContentChunk::WebSearchToolResult => {
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.push_assistant_content_block(
|
||||
|
@ -602,7 +594,14 @@ impl ClaudeAgentSession {
|
|||
"Should not get tool results with role: assistant. should we handle this?"
|
||||
);
|
||||
}
|
||||
ContentChunk::Image | ContentChunk::Document => {
|
||||
ContentChunk::Image { source } => {
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.push_assistant_content_block(source.into(), false, cx)
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
ContentChunk::Document => {
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.push_assistant_content_block(
|
||||
|
@ -768,14 +767,44 @@ enum ContentChunk {
|
|||
thinking: String,
|
||||
},
|
||||
RedactedThinking,
|
||||
Image {
|
||||
source: ImageSource,
|
||||
},
|
||||
// TODO
|
||||
Image,
|
||||
Document,
|
||||
WebSearchToolResult,
|
||||
#[serde(untagged)]
|
||||
UntaggedText(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
enum ImageSource {
|
||||
Base64 { data: String, media_type: String },
|
||||
Url { url: String },
|
||||
}
|
||||
|
||||
impl Into<acp::ContentBlock> for ImageSource {
|
||||
fn into(self) -> acp::ContentBlock {
|
||||
match self {
|
||||
ImageSource::Base64 { data, media_type } => {
|
||||
acp::ContentBlock::Image(acp::ImageContent {
|
||||
annotations: None,
|
||||
data,
|
||||
mime_type: media_type,
|
||||
uri: None,
|
||||
})
|
||||
}
|
||||
ImageSource::Url { url } => acp::ContentBlock::Image(acp::ImageContent {
|
||||
annotations: None,
|
||||
data: "".to_string(),
|
||||
mime_type: "".to_string(),
|
||||
uri: Some(url),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ContentChunk {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
|
@ -784,7 +813,7 @@ impl Display for ContentChunk {
|
|||
ContentChunk::RedactedThinking => write!(f, "Thinking: [REDACTED]"),
|
||||
ContentChunk::UntaggedText(text) => write!(f, "{}", text),
|
||||
ContentChunk::ToolResult { content, .. } => write!(f, "{}", content),
|
||||
ContentChunk::Image
|
||||
ContentChunk::Image { .. }
|
||||
| ContentChunk::Document
|
||||
| ContentChunk::ToolUse { .. }
|
||||
| ContentChunk::WebSearchToolResult => {
|
||||
|
@ -896,6 +925,75 @@ impl Display for ResultErrorType {
|
|||
}
|
||||
}
|
||||
|
||||
fn acp_content_to_claude(prompt: Vec<acp::ContentBlock>) -> Vec<ContentChunk> {
|
||||
let mut content = Vec::with_capacity(prompt.len());
|
||||
let mut context = Vec::with_capacity(prompt.len());
|
||||
|
||||
for chunk in prompt {
|
||||
match chunk {
|
||||
acp::ContentBlock::Text(text_content) => {
|
||||
content.push(ContentChunk::Text {
|
||||
text: text_content.text,
|
||||
});
|
||||
}
|
||||
acp::ContentBlock::ResourceLink(resource_link) => {
|
||||
match MentionUri::parse(&resource_link.uri) {
|
||||
Ok(uri) => {
|
||||
content.push(ContentChunk::Text {
|
||||
text: format!("{}", uri.as_link()),
|
||||
});
|
||||
}
|
||||
Err(_) => {
|
||||
content.push(ContentChunk::Text {
|
||||
text: resource_link.uri,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
acp::ContentBlock::Resource(resource) => match resource.resource {
|
||||
acp::EmbeddedResourceResource::TextResourceContents(resource) => {
|
||||
match MentionUri::parse(&resource.uri) {
|
||||
Ok(uri) => {
|
||||
content.push(ContentChunk::Text {
|
||||
text: format!("{}", uri.as_link()),
|
||||
});
|
||||
}
|
||||
Err(_) => {
|
||||
content.push(ContentChunk::Text {
|
||||
text: resource.uri.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
context.push(ContentChunk::Text {
|
||||
text: format!(
|
||||
"\n<context ref=\"{}\">\n{}\n</context>",
|
||||
resource.uri, resource.text
|
||||
),
|
||||
});
|
||||
}
|
||||
acp::EmbeddedResourceResource::BlobResourceContents(_) => {
|
||||
// Unsupported by SDK
|
||||
}
|
||||
},
|
||||
acp::ContentBlock::Image(acp::ImageContent {
|
||||
data, mime_type, ..
|
||||
}) => content.push(ContentChunk::Image {
|
||||
source: ImageSource::Base64 {
|
||||
data,
|
||||
media_type: mime_type,
|
||||
},
|
||||
}),
|
||||
acp::ContentBlock::Audio(_) => {
|
||||
// Unsupported by SDK
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
content.extend(context);
|
||||
content
|
||||
}
|
||||
|
||||
fn new_request_id() -> String {
|
||||
use rand::Rng;
|
||||
// In the Claude Code TS SDK they just generate a random 12 character string,
|
||||
|
@ -1112,4 +1210,100 @@ pub(crate) mod tests {
|
|||
_ => panic!("Expected ToolResult variant"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_acp_content_to_claude() {
|
||||
let acp_content = vec![
|
||||
acp::ContentBlock::Text(acp::TextContent {
|
||||
text: "Hello world".to_string(),
|
||||
annotations: None,
|
||||
}),
|
||||
acp::ContentBlock::Image(acp::ImageContent {
|
||||
data: "base64data".to_string(),
|
||||
mime_type: "image/png".to_string(),
|
||||
annotations: None,
|
||||
uri: None,
|
||||
}),
|
||||
acp::ContentBlock::ResourceLink(acp::ResourceLink {
|
||||
uri: "file:///path/to/example.rs".to_string(),
|
||||
name: "example.rs".to_string(),
|
||||
annotations: None,
|
||||
description: None,
|
||||
mime_type: None,
|
||||
size: None,
|
||||
title: None,
|
||||
}),
|
||||
acp::ContentBlock::Resource(acp::EmbeddedResource {
|
||||
annotations: None,
|
||||
resource: acp::EmbeddedResourceResource::TextResourceContents(
|
||||
acp::TextResourceContents {
|
||||
mime_type: None,
|
||||
text: "fn main() { println!(\"Hello!\"); }".to_string(),
|
||||
uri: "file:///path/to/code.rs".to_string(),
|
||||
},
|
||||
),
|
||||
}),
|
||||
acp::ContentBlock::ResourceLink(acp::ResourceLink {
|
||||
uri: "invalid_uri_format".to_string(),
|
||||
name: "invalid.txt".to_string(),
|
||||
annotations: None,
|
||||
description: None,
|
||||
mime_type: None,
|
||||
size: None,
|
||||
title: None,
|
||||
}),
|
||||
];
|
||||
|
||||
let claude_content = acp_content_to_claude(acp_content);
|
||||
|
||||
assert_eq!(claude_content.len(), 6);
|
||||
|
||||
match &claude_content[0] {
|
||||
ContentChunk::Text { text } => assert_eq!(text, "Hello world"),
|
||||
_ => panic!("Expected Text chunk"),
|
||||
}
|
||||
|
||||
match &claude_content[1] {
|
||||
ContentChunk::Image { source } => match source {
|
||||
ImageSource::Base64 { data, media_type } => {
|
||||
assert_eq!(data, "base64data");
|
||||
assert_eq!(media_type, "image/png");
|
||||
}
|
||||
_ => panic!("Expected Base64 image source"),
|
||||
},
|
||||
_ => panic!("Expected Image chunk"),
|
||||
}
|
||||
|
||||
match &claude_content[2] {
|
||||
ContentChunk::Text { text } => {
|
||||
assert!(text.contains("example.rs"));
|
||||
assert!(text.contains("file:///path/to/example.rs"));
|
||||
}
|
||||
_ => panic!("Expected Text chunk for ResourceLink"),
|
||||
}
|
||||
|
||||
match &claude_content[3] {
|
||||
ContentChunk::Text { text } => {
|
||||
assert!(text.contains("code.rs"));
|
||||
assert!(text.contains("file:///path/to/code.rs"));
|
||||
}
|
||||
_ => panic!("Expected Text chunk for Resource"),
|
||||
}
|
||||
|
||||
match &claude_content[4] {
|
||||
ContentChunk::Text { text } => {
|
||||
assert_eq!(text, "invalid_uri_format");
|
||||
}
|
||||
_ => panic!("Expected Text chunk for invalid URI"),
|
||||
}
|
||||
|
||||
match &claude_content[5] {
|
||||
ContentChunk::Text { text } => {
|
||||
assert!(text.contains("<context ref=\"file:///path/to/code.rs\">"));
|
||||
assert!(text.contains("fn main() { println!(\"Hello!\"); }"));
|
||||
assert!(text.contains("</context>"));
|
||||
}
|
||||
_ => panic!("Expected Text chunk for context"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue