acp: Support embedded resources in tool call content (#59821)

We weren't handling these really before, just rendering the URI. This
should provide best-effort support for what we can render.

<img width="800" height="359" alt="image"
src="https://github.com/user-attachments/assets/51f101c7-c3be-4801-9000-62000a71faa6"
/>

(I will remove the extra labels that was just for testing)

Release Notes:

- acp: Support embedded resources in tool calls.
This commit is contained in:
Ben Brandt 2026-06-24 17:25:44 +02:00 committed by GitHub
parent 697cff57bc
commit 2df74932bc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 552 additions and 88 deletions

1
Cargo.lock generated
View file

@ -116,6 +116,7 @@ dependencies = [
"language_model",
"log",
"markdown",
"mime",
"multi_buffer",
"parking_lot",
"portable-pty",

View file

@ -392,6 +392,7 @@ mermaid_render = { path = "crates/mermaid_render" }
svg_preview = { path = "crates/svg_preview" }
media = { path = "crates/media" }
menu = { path = "crates/menu" }
mime = "0.3.17"
migrator = { path = "crates/migrator" }
mistral = { path = "crates/mistral" }
multi_buffer = { path = "crates/multi_buffer" }

View file

@ -35,6 +35,7 @@ language.workspace = true
language_model.workspace = true
log.workspace = true
markdown.workspace = true
mime.workspace = true
parking_lot = { workspace = true, optional = true }
image.workspace = true
portable-pty.workspace = true

View file

@ -957,6 +957,10 @@ pub enum ContentBlock {
Markdown {
markdown: Entity<Markdown>,
},
EmbeddedResource {
resource: acp::EmbeddedResource,
markdown: Option<Entity<Markdown>>,
},
ResourceLink {
resource_link: acp::ResourceLink,
},
@ -991,6 +995,26 @@ impl ContentBlock {
this
}
pub fn new_tool_call_content(
block: acp::ContentBlock,
language_registry: &Arc<LanguageRegistry>,
path_style: PathStyle,
cx: &mut App,
) -> Self {
match block {
acp::ContentBlock::Resource(resource) => {
if let Some((image, dimensions)) = Self::decode_embedded_resource_image(&resource) {
Self::Image { image, dimensions }
} else {
let markdown =
Self::embedded_resource_markdown(&resource, language_registry, cx);
Self::EmbeddedResource { resource, markdown }
}
}
block => Self::new(block, language_registry, path_style, cx),
}
}
pub fn append(
&mut self,
block: acp::ContentBlock,
@ -1026,6 +1050,13 @@ impl ContentBlock {
let combined = format!("{}\n{}", existing_content, new_content);
*self = Self::create_markdown_block(combined, language_registry, cx);
}
(ContentBlock::EmbeddedResource { resource, .. }, _) => {
let existing_content =
Self::embedded_resource_string_contents(resource, path_style);
let new_content = Self::block_string_contents(&block, path_style);
let combined = format!("{}\n{}", existing_content, new_content);
*self = Self::create_markdown_block(combined, language_registry, cx);
}
(ContentBlock::Image { .. }, _) => {
let new_content = Self::block_string_contents(&block, path_style);
let combined = format!("`Image`\n{}", new_content);
@ -1063,13 +1094,30 @@ impl ContentBlock {
fn decode_image(
image_content: &acp::ImageContent,
) -> Option<(Arc<gpui::Image>, Option<gpui::Size<u32>>)> {
Self::decode_image_data(&image_content.data, &image_content.mime_type)
}
fn decode_embedded_resource_image(
resource: &acp::EmbeddedResource,
) -> Option<(Arc<gpui::Image>, Option<gpui::Size<u32>>)> {
let acp::EmbeddedResourceResource::BlobResourceContents(blob) = &resource.resource else {
return None;
};
let mime_type = blob.mime_type.as_deref()?;
Self::decode_image_data(&blob.blob, mime_type)
}
fn decode_image_data(
data: &str,
mime_type: &str,
) -> Option<(Arc<gpui::Image>, Option<gpui::Size<u32>>)> {
use base64::Engine as _;
let bytes = base64::engine::general_purpose::STANDARD
.decode(image_content.data.as_bytes())
.decode(data.as_bytes())
.ok()?;
let format = gpui::ImageFormat::from_mime_type(&image_content.mime_type)?;
let format = gpui::ImageFormat::from_mime_type(mime_type)?;
let dimensions = Self::image_dimensions(&bytes, format);
Some((Arc::new(gpui::Image::from_bytes(format, bytes)), dimensions))
}
@ -1099,19 +1147,141 @@ impl ContentBlock {
cx: &mut App,
) -> ContentBlock {
ContentBlock::Markdown {
markdown: cx.new(|cx| {
Markdown::new_with_options(
content.into(),
Some(language_registry.clone()),
None,
MarkdownOptions {
render_mermaid_diagrams: true,
render_metadata_blocks: true,
..Default::default()
},
cx,
)
}),
markdown: Self::create_markdown(content, language_registry, cx),
}
}
fn create_markdown(
content: String,
language_registry: &Arc<LanguageRegistry>,
cx: &mut App,
) -> Entity<Markdown> {
cx.new(|cx| {
Markdown::new_with_options(
content.into(),
Some(language_registry.clone()),
None,
MarkdownOptions {
render_mermaid_diagrams: true,
render_metadata_blocks: true,
..Default::default()
},
cx,
)
})
}
fn embedded_resource_markdown(
resource: &acp::EmbeddedResource,
language_registry: &Arc<LanguageRegistry>,
cx: &mut App,
) -> Option<Entity<Markdown>> {
match &resource.resource {
acp::EmbeddedResourceResource::TextResourceContents(text) => Some(
Self::create_markdown(Self::text_resource_markdown(text), language_registry, cx),
),
acp::EmbeddedResourceResource::BlobResourceContents(_) => None,
_ => None,
}
}
fn text_resource_markdown(resource: &acp::TextResourceContents) -> String {
match text_resource_render_mode(resource.mime_type.as_deref()) {
TextResourceRenderMode::Markdown => resource.text.clone(),
TextResourceRenderMode::CodeBlock(language) => {
Self::fenced_code_block(&resource.text, language)
}
}
}
pub fn text_content<'a>(&'a self, cx: &'a App) -> Option<&'a str> {
match self {
ContentBlock::Markdown { markdown } => Some(markdown.read(cx).source()),
ContentBlock::EmbeddedResource { resource, .. } => match &resource.resource {
acp::EmbeddedResourceResource::TextResourceContents(text) => Some(&text.text),
acp::EmbeddedResourceResource::BlobResourceContents(_) => None,
_ => None,
},
ContentBlock::Empty
| ContentBlock::ResourceLink { .. }
| ContentBlock::Image { .. } => None,
}
}
fn fenced_code_block(text: &str, language: Option<&str>) -> String {
let fence_len = text
.as_bytes()
.chunk_by(|left, right| left == right)
.filter(|chunk| chunk.first() == Some(&b'`'))
.map(|chunk| chunk.len() + 1)
.max()
.unwrap_or(3)
.max(3);
let fence = "`".repeat(fence_len);
let mut markdown = String::new();
markdown.push_str(&fence);
if let Some(language) = language {
markdown.push_str(language);
}
markdown.push('\n');
markdown.push_str(text);
if !text.ends_with('\n') {
markdown.push('\n');
}
markdown.push_str(&fence);
markdown
}
fn embedded_resource_string_contents(
resource: &acp::EmbeddedResource,
path_style: PathStyle,
) -> String {
match &resource.resource {
acp::EmbeddedResourceResource::TextResourceContents(text) => {
Self::resource_link_md(&text.uri, path_style)
}
acp::EmbeddedResourceResource::BlobResourceContents(blob) => {
Self::resource_link_md(&blob.uri, path_style)
}
_ => String::new(),
}
}
fn embedded_resource_text(resource: &acp::EmbeddedResource) -> &str {
match &resource.resource {
acp::EmbeddedResourceResource::TextResourceContents(text) => &text.text,
acp::EmbeddedResourceResource::BlobResourceContents(blob) => &blob.uri,
_ => "",
}
}
fn embedded_resource_label(resource: &acp::EmbeddedResource) -> &str {
match &resource.resource {
acp::EmbeddedResourceResource::TextResourceContents(text) => &text.uri,
acp::EmbeddedResourceResource::BlobResourceContents(blob) => &blob.uri,
_ => "",
}
}
pub fn embedded_resource(&self) -> Option<(&acp::EmbeddedResource, Option<&Entity<Markdown>>)> {
match self {
ContentBlock::EmbeddedResource { resource, markdown } => {
Some((resource, markdown.as_ref()))
}
_ => None,
}
}
pub fn visible_content(&self, cx: &App) -> bool {
match self {
ContentBlock::Empty => false,
ContentBlock::Markdown { markdown } => !markdown.read(cx).source().trim().is_empty(),
ContentBlock::EmbeddedResource { resource, markdown } => match markdown {
Some(markdown) => !markdown.read(cx).source().trim().is_empty(),
None => !Self::embedded_resource_text(resource).trim().is_empty(),
},
ContentBlock::ResourceLink { .. } | ContentBlock::Image { .. } => true,
}
}
@ -1150,6 +1320,13 @@ impl ContentBlock {
match self {
ContentBlock::Empty => "",
ContentBlock::Markdown { markdown } => markdown.read(cx).source(),
ContentBlock::EmbeddedResource { resource, markdown } => {
if let Some(markdown) = markdown {
markdown.read(cx).source()
} else {
Self::embedded_resource_label(resource)
}
}
ContentBlock::ResourceLink { resource_link } => &resource_link.uri,
ContentBlock::Image { .. } => "`Image`",
}
@ -1159,6 +1336,7 @@ impl ContentBlock {
match self {
ContentBlock::Empty => None,
ContentBlock::Markdown { markdown } => Some(markdown),
ContentBlock::EmbeddedResource { markdown, .. } => markdown.as_ref(),
ContentBlock::ResourceLink { .. } => None,
ContentBlock::Image { .. } => None,
}
@ -1179,6 +1357,61 @@ impl ContentBlock {
}
}
enum TextResourceRenderMode {
Markdown,
CodeBlock(Option<&'static str>),
}
fn text_resource_render_mode(mime_type: Option<&str>) -> TextResourceRenderMode {
let Some(mime_type) = mime_type else {
return TextResourceRenderMode::CodeBlock(None);
};
let Ok(mime) = mime_type.parse::<mime::Mime>() else {
return TextResourceRenderMode::CodeBlock(None);
};
let type_ = mime.type_().as_str();
let subtype = mime.subtype().as_str();
let suffix = mime.suffix().map(|suffix| suffix.as_str());
if matches!(
(type_, subtype),
("text", "markdown") | ("text", "x-markdown")
) {
return TextResourceRenderMode::Markdown;
}
let language = match (type_, subtype, suffix) {
(_, "json", _) | (_, _, Some("json")) => Some("json"),
(_, "xml", _) | (_, _, Some("xml")) => Some("xml"),
("text", "html", _) => Some("html"),
("text", "css", _) => Some("css"),
("text", "csv", _) => Some("csv"),
("text", "tab-separated-values", _) => Some("tsv"),
("text", "javascript", _) | ("application", "javascript", _) => Some("javascript"),
("application", "x-javascript", _) => Some("javascript"),
("text", "typescript", _) | ("application", "typescript", _) => Some("typescript"),
("text", "x-shellscript", _) | ("application", "x-shellscript", _) => Some("sh"),
("application", "x-sh", _) => Some("sh"),
("text", "x-python", _) => Some("python"),
("text", "x-rust", _) => Some("rust"),
("text", "x-go", _) => Some("go"),
("text", "x-ruby", _) => Some("ruby"),
("text", "x-c", _) => Some("c"),
// `mime` parses `text/x-c++` as subtype `x-c+` with an empty suffix.
("text", "x-c+", Some("")) => Some("cpp"),
("text", "plain", _) => None,
("text", _, _) => None,
("application", "graphql", _) => Some("graphql"),
("application", "toml", _) => Some("toml"),
("application", "yaml", _) | ("application", "x-yaml", _) => Some("yaml"),
(_, _, Some("yaml" | "yml")) => Some("yaml"),
_ => return TextResourceRenderMode::CodeBlock(None),
};
TextResourceRenderMode::CodeBlock(language)
}
#[derive(Debug)]
pub enum ToolCallContent {
ContentBlock(ContentBlock),
@ -1195,14 +1428,14 @@ impl ToolCallContent {
cx: &mut App,
) -> Result<Option<Self>> {
match content {
acp::ToolCallContent::Content(acp::Content { content, .. }) => {
Ok(Some(Self::ContentBlock(ContentBlock::new(
acp::ToolCallContent::Content(acp::Content { content, .. }) => Ok(Some(
Self::ContentBlock(ContentBlock::new_tool_call_content(
content,
&language_registry,
path_style,
cx,
))))
}
)),
)),
acp::ToolCallContent::Diff(diff) => Ok(Some(Self::Diff(cx.new(|cx| {
Diff::finalized(
diff.path.to_string_lossy().into_owned(),
@ -3858,6 +4091,206 @@ mod tests {
});
}
#[test]
fn text_resource_markdown_uses_mime_type_for_code_blocks() {
let shell = acp::TextResourceContents::new("echo 'hello from exec test'", "tool://preview")
.mime_type("text/x-shellscript".to_string());
assert_eq!(
ContentBlock::text_resource_markdown(&shell),
"```sh\necho 'hello from exec test'\n```"
);
let markdown = acp::TextResourceContents::new("**approval** requested", "tool://preview")
.mime_type("text/markdown".to_string());
assert_eq!(
ContentBlock::text_resource_markdown(&markdown),
"**approval** requested"
);
let plain = acp::TextResourceContents::new("plain preview", "tool://preview")
.mime_type("text/plain".to_string());
assert_eq!(
ContentBlock::text_resource_markdown(&plain),
"```\nplain preview\n```"
);
let cpp = acp::TextResourceContents::new("int main() {}", "tool://preview")
.mime_type("text/x-c++; charset=utf-8".to_string());
assert_eq!(
ContentBlock::text_resource_markdown(&cpp),
"```cpp\nint main() {}\n```"
);
let untyped = acp::TextResourceContents::new("# plain preview", "tool://preview");
assert_eq!(
ContentBlock::text_resource_markdown(&untyped),
"```\n# plain preview\n```"
);
}
#[gpui::test]
async fn test_tool_call_content_preserves_embedded_text_resource(
cx: &mut gpui::TestAppContext,
) {
init_test(cx);
cx.update(|cx| {
let language_registry =
Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
let content = acp::ContentBlock::Resource(acp::EmbeddedResource::new(
acp::EmbeddedResourceResource::TextResourceContents(
acp::TextResourceContents::new("echo 'hello from exec test'", "tool://preview")
.mime_type("text/x-shellscript".to_string()),
),
));
let block = ContentBlock::new_tool_call_content(
content,
&language_registry,
PathStyle::local(),
cx,
);
let ContentBlock::EmbeddedResource { resource, markdown } = &block else {
panic!("expected embedded resource block, got {block:?}");
};
match &resource.resource {
acp::EmbeddedResourceResource::TextResourceContents(text) => {
assert_eq!(text.text, "echo 'hello from exec test'");
assert_eq!(text.uri, "tool://preview");
assert_eq!(text.mime_type.as_deref(), Some("text/x-shellscript"));
}
other => panic!("expected text resource contents, got {other:?}"),
}
let markdown = markdown
.as_ref()
.expect("text resources should have renderable markdown")
.read(cx)
.source()
.to_string();
assert_eq!(markdown, "```sh\necho 'hello from exec test'\n```");
assert_eq!(
block.to_markdown(cx),
"```sh\necho 'hello from exec test'\n```"
);
assert_eq!(block.text_content(cx), Some("echo 'hello from exec test'"));
let untyped = ContentBlock::new_tool_call_content(
acp::ContentBlock::Resource(acp::EmbeddedResource::new(
acp::EmbeddedResourceResource::TextResourceContents(
acp::TextResourceContents::new("# plain preview", "tool://preview"),
),
)),
&language_registry,
PathStyle::local(),
cx,
);
assert_eq!(untyped.to_markdown(cx), "```\n# plain preview\n```");
assert_eq!(untyped.text_content(cx), Some("# plain preview"));
});
}
#[gpui::test]
async fn test_tool_call_content_renders_embedded_image_blob_resource(
cx: &mut gpui::TestAppContext,
) {
init_test(cx);
cx.update(|cx| {
let language_registry =
Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
let image_blob = acp::ContentBlock::Resource(acp::EmbeddedResource::new(
acp::EmbeddedResourceResource::BlobResourceContents(
acp::BlobResourceContents::new(
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==",
"tool://preview.png",
)
.mime_type("image/png".to_string()),
),
));
let block = ContentBlock::new_tool_call_content(
image_blob,
&language_registry,
PathStyle::local(),
cx,
);
let ContentBlock::Image { image, dimensions } = &block else {
panic!("expected image block, got {block:?}");
};
assert_eq!(image.format(), gpui::ImageFormat::Png);
assert_eq!(
dimensions.as_ref().map(|size| (size.width, size.height)),
Some((1, 1))
);
assert_eq!(block.to_markdown(cx), "`Image`");
assert_eq!(block.text_content(cx), None);
});
}
#[gpui::test]
async fn test_tool_call_content_falls_back_for_non_image_blob_resource(
cx: &mut gpui::TestAppContext,
) {
init_test(cx);
cx.update(|cx| {
let language_registry =
Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
let archive_blob = acp::ContentBlock::Resource(acp::EmbeddedResource::new(
acp::EmbeddedResourceResource::BlobResourceContents(
acp::BlobResourceContents::new("not an image", "tool://archive.bin")
.mime_type("application/octet-stream".to_string()),
),
));
let block = ContentBlock::new_tool_call_content(
archive_blob,
&language_registry,
PathStyle::local(),
cx,
);
let ContentBlock::EmbeddedResource { resource, markdown } = &block else {
panic!("expected embedded resource block, got {block:?}");
};
assert!(markdown.is_none());
match &resource.resource {
acp::EmbeddedResourceResource::BlobResourceContents(blob) => {
assert_eq!(blob.uri, "tool://archive.bin");
assert_eq!(blob.mime_type.as_deref(), Some("application/octet-stream"));
}
other => panic!("expected blob resource contents, got {other:?}"),
}
assert_eq!(block.to_markdown(cx), "tool://archive.bin");
assert_eq!(block.text_content(cx), None);
let invalid_image_blob = acp::ContentBlock::Resource(acp::EmbeddedResource::new(
acp::EmbeddedResourceResource::BlobResourceContents(
acp::BlobResourceContents::new("not-base64", "tool://preview.png")
.mime_type("image/png".to_string()),
),
));
let invalid = ContentBlock::new_tool_call_content(
invalid_image_blob,
&language_registry,
PathStyle::local(),
cx,
);
let ContentBlock::EmbeddedResource { resource, markdown } = &invalid else {
panic!("expected embedded resource block, got {invalid:?}");
};
assert!(markdown.is_none());
assert_eq!(
ContentBlock::embedded_resource_label(resource),
"tool://preview.png"
);
assert_eq!(invalid.to_markdown(cx), "tool://preview.png");
});
}
#[test]
fn sandbox_authorization_details_deserialize_legacy_network_bool() {
// Older builds persisted `network: bool`; the `alias` on
@ -6100,6 +6533,9 @@ mod tests {
ContentBlock::ResourceLink { .. } => {
panic!("Expected markdown content, got resource link")
}
ContentBlock::EmbeddedResource { .. } => {
panic!("Expected markdown content, got embedded resource")
}
ContentBlock::Image { .. } => {
panic!("Expected markdown content, got image")
}

View file

@ -920,8 +920,13 @@ fn collect_markdowns(
ToolCallContent::ContentBlock(ContentBlock::Markdown { markdown }) => {
Some(markdown.clone())
}
ToolCallContent::ContentBlock(ContentBlock::EmbeddedResource {
markdown: Some(markdown),
..
}) => Some(markdown.clone()),
ToolCallContent::ContentBlock(
ContentBlock::Empty
| ContentBlock::EmbeddedResource { markdown: None, .. }
| ContentBlock::ResourceLink { .. }
| ContentBlock::Image { .. },
)

View file

@ -9,7 +9,7 @@ use agent_client_protocol::schema::v1 as acp;
use std::cell::RefCell;
use acp_thread::{
ContentBlock, PlanEntry, SandboxAuthorizationDetails, SandboxFallbackAuthorizationDetails,
PlanEntry, SandboxAuthorizationDetails, SandboxFallbackAuthorizationDetails,
SandboxNotAppliedReason,
};
use agent::{
@ -6179,15 +6179,7 @@ impl ThreadView {
if matches!(tool_call.status, ToolCallStatus::Canceled) {
let has_visible_content =
tool_call.content.iter().any(|content| match content {
ToolCallContent::ContentBlock(block) => match block {
ContentBlock::Empty => false,
ContentBlock::Markdown { markdown } => {
!markdown.read(cx).source().trim().is_empty()
}
ContentBlock::ResourceLink { .. } | ContentBlock::Image { .. } => {
true
}
},
ToolCallContent::ContentBlock(block) => block.visible_content(cx),
ToolCallContent::Diff(_) | ToolCallContent::Terminal(_) => true,
});
if !has_visible_content {
@ -9527,7 +9519,18 @@ impl ThreadView {
) -> AnyElement {
match content {
ToolCallContent::ContentBlock(content) => {
if let Some(resource_link) = content.resource_link() {
if let Some((resource, markdown)) = content.embedded_resource() {
self.render_embedded_resource_output(
resource,
markdown.cloned(),
entry_ix,
context_ix,
tool_call,
card_layout,
window,
cx,
)
} else if let Some(resource_link) = content.resource_link() {
self.render_resource_link(resource_link, cx)
} else if let Some(markdown) = content.markdown() {
self.render_markdown_output(
@ -9539,16 +9542,9 @@ impl ThreadView {
window,
cx,
)
} else if let Some((image, dimensions)) = content.image() {
} else if let Some((image, _)) = content.image() {
let location = tool_call.locations.first().cloned();
self.render_image_output(
entry_ix,
image.clone(),
dimensions,
location,
card_layout,
cx,
)
self.render_image_output(entry_ix, image.clone(), location, card_layout, cx)
} else {
Empty.into_any_element()
}
@ -9569,6 +9565,60 @@ impl ThreadView {
}
}
fn render_embedded_resource_output(
&self,
resource: &acp::EmbeddedResource,
markdown: Option<Entity<Markdown>>,
entry_ix: usize,
context_ix: usize,
tool_call: &ToolCall,
card_layout: bool,
window: &Window,
cx: &Context<Self>,
) -> AnyElement {
if let Some(markdown) = markdown {
return self.render_markdown_output(
markdown,
entry_ix,
context_ix,
tool_call,
card_layout,
window,
cx,
);
}
let uri = match &resource.resource {
acp::EmbeddedResourceResource::BlobResourceContents(blob) => blob.uri.as_str(),
acp::EmbeddedResourceResource::TextResourceContents(text) => text.uri.as_str(),
_ => "",
};
v_flex()
.gap_1()
.map(|this| {
if card_layout {
this.p_2().when(context_ix > 0, |this| {
this.border_t_1()
.border_color(self.tool_card_border_color(cx))
})
} else {
this.ml(rems(0.4))
.px_3p5()
.border_l_1()
.border_color(self.tool_card_border_color(cx))
}
})
.when(!uri.is_empty(), |this| {
this.child(
Label::new(uri.to_string())
.size(LabelSize::XSmall)
.color(Color::Muted),
)
})
.into_any_element()
}
fn render_resource_link(
&self,
resource_link: &acp::ResourceLink,
@ -9762,28 +9812,10 @@ impl ThreadView {
&self,
entry_ix: usize,
image: Arc<gpui::Image>,
dimensions: Option<gpui::Size<u32>>,
location: Option<acp::ToolCallLocation>,
card_layout: bool,
cx: &Context<Self>,
) -> AnyElement {
let format_name = match image.format() {
gpui::ImageFormat::Png => "PNG",
gpui::ImageFormat::Jpeg => "JPEG",
gpui::ImageFormat::Webp => "WebP",
gpui::ImageFormat::Gif => "GIF",
gpui::ImageFormat::Svg => "SVG",
gpui::ImageFormat::Bmp => "BMP",
gpui::ImageFormat::Tiff => "TIFF",
gpui::ImageFormat::Ico => "ICO",
gpui::ImageFormat::Pnm => "PNM",
};
let dimensions_label = if let Some(size) = dimensions {
format!("{}×{} {}", size.width, size.height, format_name)
} else {
format_name.into()
};
v_flex()
.gap_2()
.map(|this| {
@ -9796,27 +9828,17 @@ impl ThreadView {
.border_color(self.tool_card_border_color(cx))
}
})
.child(
h_flex()
.w_full()
.justify_between()
.items_center()
.child(
Label::new(dimensions_label)
.size(LabelSize::XSmall)
.color(Color::Muted)
.buffer_font(cx),
)
.when_some(location, |this, _loc| {
this.child(
Button::new(("go-to-file", entry_ix), "Go to File")
.label_size(LabelSize::Small)
.on_click(cx.listener(move |this, _, window, cx| {
this.open_tool_call_location(entry_ix, 0, window, cx);
})),
)
}),
)
.when_some(location, |this, _loc| {
this.child(
h_flex().w_full().justify_end().child(
Button::new(("go-to-file", entry_ix), "Go to File")
.label_size(LabelSize::Small)
.on_click(cx.listener(move |this, _, window, cx| {
this.open_tool_call_location(entry_ix, 0, window, cx);
})),
),
)
})
.child(
img(image)
.max_w_96()
@ -9906,8 +9928,8 @@ impl ThreadView {
let is_cancelled = matches!(tool_call.status, ToolCallStatus::Canceled)
|| tool_call.content.iter().any(|c| match c {
ToolCallContent::ContentBlock(ContentBlock::Markdown { markdown }) => {
markdown.read(cx).source() == "User canceled"
ToolCallContent::ContentBlock(block) => {
block.text_content(cx) == Some("User canceled")
}
_ => false,
});
@ -10284,14 +10306,12 @@ impl ThreadView {
if matches!(status, ToolCallStatus::Failed) {
tool_call.content.iter().find_map(|content| {
if let ToolCallContent::ContentBlock(block) = content {
if let acp_thread::ContentBlock::Markdown { markdown } = block {
let source = markdown.read(cx).source().to_string();
if !source.is_empty() {
if source == "User canceled" {
return None;
} else {
return Some(SharedString::from(source));
}
if let Some(source) = block.text_content(cx).filter(|source| !source.is_empty())
{
if source == "User canceled" {
return None;
} else {
return Some(SharedString::from(source));
}
}
}