From 2df74932bc6666e7a00e877d375dafb39a7a9273 Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Wed, 24 Jun 2026 17:25:44 +0200 Subject: [PATCH] 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. image (I will remove the extra labels that was just for testing) Release Notes: - acp: Support embedded resources in tool calls. --- Cargo.lock | 1 + Cargo.toml | 1 + crates/acp_thread/Cargo.toml | 1 + crates/acp_thread/src/acp_thread.rs | 474 +++++++++++++++++- .../conversation_view/thread_search_bar.rs | 5 + .../src/conversation_view/thread_view.rs | 158 +++--- 6 files changed, 552 insertions(+), 88 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 069a21006e7..4e1f6c99023 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -116,6 +116,7 @@ dependencies = [ "language_model", "log", "markdown", + "mime", "multi_buffer", "parking_lot", "portable-pty", diff --git a/Cargo.toml b/Cargo.toml index 4fb73966008..611ecf2a258 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" } diff --git a/crates/acp_thread/Cargo.toml b/crates/acp_thread/Cargo.toml index d92fd6b1fec..3c4f592a554 100644 --- a/crates/acp_thread/Cargo.toml +++ b/crates/acp_thread/Cargo.toml @@ -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 diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index cabdddfba19..1efe43188b4 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -957,6 +957,10 @@ pub enum ContentBlock { Markdown { markdown: Entity, }, + EmbeddedResource { + resource: acp::EmbeddedResource, + markdown: Option>, + }, ResourceLink { resource_link: acp::ResourceLink, }, @@ -991,6 +995,26 @@ impl ContentBlock { this } + pub fn new_tool_call_content( + block: acp::ContentBlock, + language_registry: &Arc, + 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, Option>)> { + Self::decode_image_data(&image_content.data, &image_content.mime_type) + } + + fn decode_embedded_resource_image( + resource: &acp::EmbeddedResource, + ) -> Option<(Arc, Option>)> { + 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, Option>)> { 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, + cx: &mut App, + ) -> Entity { + 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, + cx: &mut App, + ) -> Option> { + 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>)> { + 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::() 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> { 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") } diff --git a/crates/agent_ui/src/conversation_view/thread_search_bar.rs b/crates/agent_ui/src/conversation_view/thread_search_bar.rs index 80925e4b793..6563046ca1d 100644 --- a/crates/agent_ui/src/conversation_view/thread_search_bar.rs +++ b/crates/agent_ui/src/conversation_view/thread_search_bar.rs @@ -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 { .. }, ) diff --git a/crates/agent_ui/src/conversation_view/thread_view.rs b/crates/agent_ui/src/conversation_view/thread_view.rs index 1a0137e001d..553519fb34c 100644 --- a/crates/agent_ui/src/conversation_view/thread_view.rs +++ b/crates/agent_ui/src/conversation_view/thread_view.rs @@ -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>, + entry_ix: usize, + context_ix: usize, + tool_call: &ToolCall, + card_layout: bool, + window: &Window, + cx: &Context, + ) -> 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, - dimensions: Option>, location: Option, card_layout: bool, cx: &Context, ) -> 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)); } } }