Send a steer into the running turn, and put an image under its call
**The queue was holding messages the CLI would have taken.** Two claims in this file contradicted each other: the module header said a mid-turn message is injected at the next tool boundary -- "the behavior this app exists for" -- and `Queue`'s own doc said a line written mid-turn simply becomes the next turn. The code followed the second, parking every message until `Status::Idle`, which is the end of the whole turn. Measured rather than argued, twice. Writing a line straight into a live session's stdin fifo mid-turn produced one `result` for the whole thing, so it was consumed inside that turn, not as a new one. The header was right and the queue was built on the wrong claim. The cost was exactly what Bryan reported: he steered after the second tool call and it sat unread until every remaining call had finished. Measured before and after on the same three-step turn -- steer sent at +13s, recorded at +24.7s before this change and at +14.1s after, which is the next tool boundary. So the line goes out immediately. What stays behind is the *announcement*: the CLI says nothing on stdout about having read a message, so `MessageTaken` now waits for the next assistant text or tool call, which is proof another model call happened and the steer was in it. That keeps a held message drawn below the working indicator until the session has actually taken it -- the thing that mattered when this was last changed -- without delaying the message to get it. Idle counts too, and is the case that must not be missed: a message written after a turn's last model call has no later output to prove anything. `closed` is untouched, and `Queue::close` still reports held messages by name rather than dropping them. **An image now names the call that produced it.** `Event::Image` gains `about`, the `tool_use_id` from the tool result it came out of, so a screenshot is drawn inside that call's card instead of floating beside it -- pairing them by position is what a page boundary breaks. `None` for a person's own attachment, which belongs to no call. The import path threads it through as well, so replayed history reads the same as live. Images show whether the card is open or closed: a call whose result *is* a picture says less closed than the one line it replaced. Verified on the emulator against a real haiku turn: the checkerboard sits inside `Read /tmp/tiny.png`, and the steer sits between that call and the next, where it was taken. 53 tests, clippy, rustfmt, lint and ktfmt clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
184b6fc6a6
commit
42131c75d6
9 files changed
+164
-56
No files matched your search
@@ -441,7 +441,7 @@ fn push_user(events: &mut Vec<Event>, content: &Value, session_dir: &std::path::
|
||||
for block in blocks {
|
||||
// A picture the person attached to their own message, rather
|
||||
// than one a tool produced. Same block shape, one level up.
|
||||
push_images(events, std::slice::from_ref(block), session_dir);
|
||||
push_images(events, std::slice::from_ref(block), session_dir, None);
|
||||
if block.get("type").and_then(Value::as_str) == Some("tool_result")
|
||||
&& let Some(id) = block.get("tool_use_id").and_then(Value::as_str)
|
||||
{
|
||||
@@ -449,7 +449,7 @@ fn push_user(events: &mut Vec<Event>, content: &Value, session_dir: &std::path::
|
||||
// a screenshot belongs to the call that took it, and after
|
||||
// the result it reads as belonging to whatever came next.
|
||||
if let Some(Value::Array(parts)) = block.get("content") {
|
||||
push_images(events, parts, session_dir);
|
||||
push_images(events, parts, session_dir, Some(id));
|
||||
}
|
||||
events.push(Event::ToolEnd {
|
||||
id: id.to_string(),
|
||||
@@ -465,12 +465,24 @@ fn push_user(events: &mut Vec<Event>, content: &Value, session_dir: &std::path::
|
||||
}
|
||||
|
||||
/// Saves every image block in `parts` and references each one.
|
||||
fn push_images(events: &mut Vec<Event>, parts: &[Value], session_dir: &std::path::Path) {
|
||||
///
|
||||
/// `about` is the call the images came out of, or `None` for one a person attached
|
||||
/// to their own message -- the same distinction the live translator makes, so replayed
|
||||
/// history draws a screenshot under the call that took it exactly as a live one does.
|
||||
fn push_images(
|
||||
events: &mut Vec<Event>,
|
||||
parts: &[Value],
|
||||
session_dir: &std::path::Path,
|
||||
about: Option<&str>,
|
||||
) {
|
||||
for part in parts {
|
||||
if part.get("type").and_then(Value::as_str) == Some("image")
|
||||
&& let Some(name) = super::claude::translate::save_image(session_dir, part)
|
||||
{
|
||||
events.push(Event::Image { image: name });
|
||||
events.push(Event::Image {
|
||||
image: name,
|
||||
about: about.map(String::from),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -626,7 +638,7 @@ mod tests {
|
||||
);
|
||||
|
||||
let events = events_from(&line, dir.path());
|
||||
let Some(Event::Image { image }) = events.first() else {
|
||||
let Some(Event::Image { image, .. }) = events.first() else {
|
||||
panic!("a replayed screenshot must become an image event: {events:?}");
|
||||
};
|
||||
assert!(image.ends_with(".png"));
|
||||
|
||||
Reference in new issue
Block a user