agent: Do not decode images during render (#56866)

Turns out we were creating an ImageDecoder on every frame (added in
#46167) when a tool returned an image as output, because we were trying
to get its dimensions. That is now cached on `ContentBlock::Image`.

Release Notes:

- N/A
This commit is contained in:
Bennet Bo Fenner 2026-05-19 11:51:42 +02:00 committed by GitHub
parent b8dce970fa
commit 8708a6fa74
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 79 additions and 59 deletions

View file

@ -13,7 +13,7 @@ path = "src/acp_thread.rs"
doctest = false doctest = false
[features] [features]
test-support = ["gpui/test-support", "project/test-support", "dep:parking_lot", "dep:image"] test-support = ["gpui/test-support", "project/test-support", "dep:parking_lot"]
[dependencies] [dependencies]
action_log.workspace = true action_log.workspace = true
@ -35,7 +35,7 @@ language_model.workspace = true
log.workspace = true log.workspace = true
markdown.workspace = true markdown.workspace = true
parking_lot = { workspace = true, optional = true } parking_lot = { workspace = true, optional = true }
image = { workspace = true, optional = true } image.workspace = true
portable-pty.workspace = true portable-pty.workspace = true
project.workspace = true project.workspace = true
prompt_store.workspace = true prompt_store.workspace = true

View file

@ -648,9 +648,16 @@ impl Display for ToolCallStatus {
#[derive(Debug, PartialEq, Clone)] #[derive(Debug, PartialEq, Clone)]
pub enum ContentBlock { pub enum ContentBlock {
Empty, Empty,
Markdown { markdown: Entity<Markdown> }, Markdown {
ResourceLink { resource_link: acp::ResourceLink }, markdown: Entity<Markdown>,
Image { image: Arc<gpui::Image> }, },
ResourceLink {
resource_link: acp::ResourceLink,
},
Image {
image: Arc<gpui::Image>,
dimensions: Option<gpui::Size<u32>>,
},
} }
impl ContentBlock { impl ContentBlock {
@ -692,8 +699,8 @@ impl ContentBlock {
}; };
} }
(ContentBlock::Empty, acp::ContentBlock::Image(image_content)) => { (ContentBlock::Empty, acp::ContentBlock::Image(image_content)) => {
if let Some(image) = Self::decode_image(image_content) { if let Some((image, dimensions)) = Self::decode_image(image_content) {
*self = ContentBlock::Image { image }; *self = ContentBlock::Image { image, dimensions };
} else { } else {
let new_content = Self::image_md(image_content); let new_content = Self::image_md(image_content);
*self = Self::create_markdown_block(new_content, language_registry, cx); *self = Self::create_markdown_block(new_content, language_registry, cx);
@ -721,14 +728,36 @@ impl ContentBlock {
} }
} }
fn decode_image(image_content: &acp::ImageContent) -> Option<Arc<gpui::Image>> { fn decode_image(
image_content: &acp::ImageContent,
) -> Option<(Arc<gpui::Image>, Option<gpui::Size<u32>>)> {
use base64::Engine as _; use base64::Engine as _;
let bytes = base64::engine::general_purpose::STANDARD let bytes = base64::engine::general_purpose::STANDARD
.decode(image_content.data.as_bytes()) .decode(image_content.data.as_bytes())
.ok()?; .ok()?;
let format = gpui::ImageFormat::from_mime_type(&image_content.mime_type)?; let format = gpui::ImageFormat::from_mime_type(&image_content.mime_type)?;
Some(Arc::new(gpui::Image::from_bytes(format, bytes))) let dimensions = Self::image_dimensions(&bytes, format);
Some((Arc::new(gpui::Image::from_bytes(format, bytes)), dimensions))
}
fn image_dimensions(bytes: &[u8], format: gpui::ImageFormat) -> Option<gpui::Size<u32>> {
let format = match format {
gpui::ImageFormat::Png => image::ImageFormat::Png,
gpui::ImageFormat::Jpeg => image::ImageFormat::Jpeg,
gpui::ImageFormat::Webp => image::ImageFormat::WebP,
gpui::ImageFormat::Gif => image::ImageFormat::Gif,
gpui::ImageFormat::Svg => return None,
gpui::ImageFormat::Bmp => image::ImageFormat::Bmp,
gpui::ImageFormat::Tiff => image::ImageFormat::Tiff,
gpui::ImageFormat::Ico => image::ImageFormat::Ico,
gpui::ImageFormat::Pnm => image::ImageFormat::Pnm,
};
image::ImageReader::with_format(std::io::Cursor::new(bytes), format)
.into_dimensions()
.ok()
.map(|(width, height)| gpui::Size { width, height })
} }
fn create_markdown_block( fn create_markdown_block(
@ -808,9 +837,9 @@ impl ContentBlock {
} }
} }
pub fn image(&self) -> Option<&Arc<gpui::Image>> { pub fn image(&self) -> Option<(&Arc<gpui::Image>, Option<gpui::Size<u32>>)> {
match self { match self {
ContentBlock::Image { image } => Some(image), ContentBlock::Image { image, dimensions } => Some((image, *dimensions)),
_ => None, _ => None,
} }
} }
@ -895,7 +924,7 @@ impl ToolCallContent {
} }
} }
pub fn image(&self) -> Option<&Arc<gpui::Image>> { pub fn image(&self) -> Option<(&Arc<gpui::Image>, Option<gpui::Size<u32>>)> {
match self { match self {
Self::ContentBlock(content) => content.image(), Self::ContentBlock(content) => content.image(),
_ => None, _ => None,

View file

@ -6446,7 +6446,6 @@ impl ThreadView {
content_ix, content_ix,
tool_call, tool_call,
use_card_layout, use_card_layout,
has_image_content,
failed_or_canceled, failed_or_canceled,
focus_handle, focus_handle,
window, window,
@ -6578,7 +6577,6 @@ impl ThreadView {
content_ix, content_ix,
tool_call, tool_call,
use_card_layout, use_card_layout,
has_image_content,
failed_or_canceled, failed_or_canceled,
focus_handle, focus_handle,
window, window,
@ -7570,7 +7568,6 @@ impl ThreadView {
context_ix: usize, context_ix: usize,
tool_call: &ToolCall, tool_call: &ToolCall,
card_layout: bool, card_layout: bool,
is_image_tool_call: bool,
has_failed: bool, has_failed: bool,
focus_handle: &FocusHandle, focus_handle: &FocusHandle,
window: &Window, window: &Window,
@ -7589,14 +7586,14 @@ impl ThreadView {
window, window,
cx, cx,
) )
} else if let Some(image) = content.image() { } else if let Some((image, dimensions)) = content.image() {
let location = tool_call.locations.first().cloned(); let location = tool_call.locations.first().cloned();
self.render_image_output( self.render_image_output(
entry_ix, entry_ix,
image.clone(), image.clone(),
dimensions,
location, location,
card_layout, card_layout,
is_image_tool_call,
cx, cx,
) )
} else { } else {
@ -7778,30 +7775,26 @@ impl ThreadView {
&self, &self,
entry_ix: usize, entry_ix: usize,
image: Arc<gpui::Image>, image: Arc<gpui::Image>,
dimensions: Option<gpui::Size<u32>>,
location: Option<acp::ToolCallLocation>, location: Option<acp::ToolCallLocation>,
card_layout: bool, card_layout: bool,
show_dimensions: bool,
cx: &Context<Self>, cx: &Context<Self>,
) -> AnyElement { ) -> AnyElement {
let dimensions_label = if show_dimensions { let format_name = match image.format() {
let format_name = match image.format() { gpui::ImageFormat::Png => "PNG",
gpui::ImageFormat::Png => "PNG", gpui::ImageFormat::Jpeg => "JPEG",
gpui::ImageFormat::Jpeg => "JPEG", gpui::ImageFormat::Webp => "WebP",
gpui::ImageFormat::Webp => "WebP", gpui::ImageFormat::Gif => "GIF",
gpui::ImageFormat::Gif => "GIF", gpui::ImageFormat::Svg => "SVG",
gpui::ImageFormat::Svg => "SVG", gpui::ImageFormat::Bmp => "BMP",
gpui::ImageFormat::Bmp => "BMP", gpui::ImageFormat::Tiff => "TIFF",
gpui::ImageFormat::Tiff => "TIFF", gpui::ImageFormat::Ico => "ICO",
gpui::ImageFormat::Ico => "ICO", gpui::ImageFormat::Pnm => "PNM",
gpui::ImageFormat::Pnm => "PNM", };
}; let dimensions_label = if let Some(size) = dimensions {
let dimensions = image::ImageReader::new(std::io::Cursor::new(image.bytes())) format!("{}×{} {}", size.width, size.height, format_name)
.with_guessed_format()
.ok()
.and_then(|reader| reader.into_dimensions().ok());
dimensions.map(|(w, h)| format!("{}×{} {}", w, h, format_name))
} else { } else {
None format_name.into()
}; };
v_flex() v_flex()
@ -7816,29 +7809,27 @@ impl ThreadView {
.border_color(self.tool_card_border_color(cx)) .border_color(self.tool_card_border_color(cx))
} }
}) })
.when(dimensions_label.is_some() || location.is_some(), |this| { .child(
this.child( h_flex()
h_flex() .w_full()
.w_full() .justify_between()
.justify_between() .items_center()
.items_center() .child(
.children(dimensions_label.map(|label| { Label::new(dimensions_label)
Label::new(label) .size(LabelSize::XSmall)
.size(LabelSize::XSmall) .color(Color::Muted)
.color(Color::Muted) .buffer_font(cx),
.buffer_font(cx) )
})) .when_some(location, |this, _loc| {
.when_some(location, |this, _loc| { this.child(
this.child( Button::new(("go-to-file", entry_ix), "Go to File")
Button::new(("go-to-file", entry_ix), "Go to File") .label_size(LabelSize::Small)
.label_size(LabelSize::Small) .on_click(cx.listener(move |this, _, window, cx| {
.on_click(cx.listener(move |this, _, window, cx| { this.open_tool_call_location(entry_ix, 0, window, cx);
this.open_tool_call_location(entry_ix, 0, window, cx); })),
})), )
) }),
}), )
)
})
.child( .child(
img(image) img(image)
.max_w_96() .max_w_96()