Keep the image on screen when its tool call joins a group

A `Read` that returns an image is a row of one call, and the moment the
session makes its next call the two become a group -- which is a different
composable in a different part of the tree, so the old subtree goes and
everything it remembered goes with it. The full-screen viewer was inside
that subtree, so somebody looking at a screenshot was thrown back to the
transcript because the session carried on working. A page of history
landing does the same thing to the same row.

What is open is a property of the screen rather than of whichever row
happened to draw the thumbnail, so it is held there now and drawn beside
the other two dialogs. Nothing that happens to rows can reach it.

The cost is one fetch when it opens, since the thumbnail's decoded bitmap
belongs to a row this no longer goes through. Paid deliberately rather than
plumbed around: it is one request for a picture somebody asked to see, and
the viewer draws the same two empty states the thumbnail does -- still
coming, and never coming -- which it previously could not have, since it
only ever opened on a bitmap already in hand.

`/tools n gap` now puts a screenshot on its first call, so the case is
reproducible rather than argued about: that command already existed to make
a run *grow* while somebody watches, and the image is what made growing
matter. Checked on the emulator with `/tools 3 30` -- opened the image on
the lone call, and it was still open a minute later with the row by then
inside a group of three, and back returned to the transcript rather than
leaving the app.
This commit is contained in:
iris committed 2026-08-31 22:41:04 -04:00
1 parent ed88bdb31f
commit bfaf5e6f38
4 files changed
+146 -42

No files matched your search

+24 -1
View File
@@ -11,7 +11,9 @@
//! looks like when a screen groups them. `gap` is seconds between one
//! call and the next, default none: it is what makes a run *grow* while
//! somebody is looking at it, which is the only way to reach the state
//! where a call opened on its own gains a neighbour.
//! where a call opened on its own gains a neighbour. The first call
//! carries a screenshot, so that state can also be reached with an image
//! open full screen -- which is where it used to close itself.
//! - `/question [text]` -- a question, exercising the answer path.
//! - `/ask` -- an AskUserQuestion call: two questions on one tool call,
//! with descriptions, a preview and a multi-select, which is the shape
@@ -482,6 +484,27 @@ impl EchoDriver {
"timeout": 5000,
}),
});
// The first call carries a screenshot, and only the
// first. That is what makes this rig cover the case a
// growing run is actually about: an image opened full
// screen from a call that is alone, and then a second
// call arriving and turning that row into a group. The
// dialog used to be inside the row, so the reader was
// thrown back to the transcript by the session making
// another tool call. Any of the calls would do; the
// first is the one that is on its own for a whole
// `gap`, which is the window somebody can open it in.
if i == 1 {
let part = serde_json::json!({
"source": {"media_type": "image/png", "data": SAMPLE_PNG}
});
if let Some(name) = super::claude::translate::save_image(&dir, &part) {
send(Event::Image {
image: name,
about: Some(id.clone()),
});
}
}
tokio::time::sleep(DELTA_DELAY).await;
send(Event::ToolEnd {
id,