Show images read by agents; keep tool-result images with Claude Code

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Sonnet 5.5 committed 2026-11-15 09:54:00 +01:00
1 parent 92a99c6a65
commit 99ba3835f3
4 files changed
+36 -3

No files matched your search

+25 -2
View File
@@ -11,7 +11,7 @@ use crate::config::ModelConfig;
use crate::permission::{Decision, Evaluator, ToolInfo, bash_prefix, is_dangerous, split_command};
use crate::provider::{ChatRequest, Provider, StreamEvent};
use crate::store::UsageRecord;
use crate::types::{Event, Message, Part, Role, Session, Usage, new_id, now_ms};
use crate::types::{Event, ImageData, Message, Part, Role, Session, Usage, new_id, now_ms};
use anyhow::{Result, anyhow, bail};
use async_trait::async_trait;
use serde_json::{Value, json};
@@ -286,6 +286,20 @@ fn result_text(content: &Value) -> String {
}
}
/// Images inside a tool result (the CLI's Read of a picture): base64 blocks.
fn result_images(content: &Value) -> Vec<ImageData> {
content
.as_array()
.into_iter()
.flatten()
.filter(|b| b["type"] == "image" && b["source"]["type"] == "base64")
.filter_map(|b| {
let data = b["source"]["data"].as_str().filter(|d| !d.is_empty())?;
Some(ImageData { media_type: b["source"]["media_type"].as_str().unwrap_or("image/png").to_string(), data: data.to_string(), blob: None, size: None })
})
.collect()
}
/// Images of the prompt as files the CLI can read (it has no inline image input in this mode).
fn save_images(cwd: &str, parts: &[Part]) -> Vec<String> {
use base64::Engine;
@@ -458,7 +472,7 @@ pub async fn run(app: Arc<App>, session_id: &str, cancel: CancellationToken) ->
let id = b["tool_use_id"].as_str().unwrap_or("").to_string();
let is_error = b["is_error"].as_bool().unwrap_or(false);
let meta = if is_error { None } else { diffs.remove(&id).map(|d| json!({"diff": d})) };
parts.push(Part::ToolResult { name: calls.get(&id).cloned().unwrap_or_default(), call_id: id, content: result_text(&b["content"]), images: vec![], is_error, meta });
parts.push(Part::ToolResult { name: calls.get(&id).cloned().unwrap_or_default(), call_id: id, content: result_text(&b["content"]), images: result_images(&b["content"]), is_error, meta });
}
if !parts.is_empty() {
let m = Message::new(Role::User, parts);
@@ -852,6 +866,15 @@ pub async fn update() -> Result<String> {
#[cfg(test)]
mod tests {
#[test]
fn tool_result_images_are_kept() {
let content = json!([{"type": "text", "text": "ok"}, {"type": "image", "source": {"type": "base64", "media_type": "image/jpeg", "data": "QUJD"}}]);
let imgs = result_images(&content);
assert_eq!(imgs.len(), 1);
assert_eq!((imgs[0].media_type.as_str(), imgs[0].data.as_str()), ("image/jpeg", "QUJD"));
assert!(result_images(&json!("plain text")).is_empty());
}
use super::*;
#[test]