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
@@ -87,6 +87,13 @@ sealed class TranscriptItem {
|
||||
* a fact rather than a match on the input.
|
||||
*/
|
||||
val ask: QuestionCard? = null,
|
||||
/**
|
||||
* Images this call's result carried, drawn under it.
|
||||
*
|
||||
* Beside it they had to be paired by position, and position is the thing a page boundary
|
||||
* breaks -- a screenshot loaded on one page and its call on the next read as unrelated.
|
||||
*/
|
||||
val images: List<String> = emptyList(),
|
||||
) : TranscriptItem()
|
||||
|
||||
data class QuestionCard(
|
||||
@@ -161,7 +168,19 @@ fun foldEvent(items: List<TranscriptItem>, event: SessionEvent): List<Transcript
|
||||
}
|
||||
is SessionEvent.Status -> items
|
||||
is SessionEvent.Error -> items + TranscriptItem.ErrorMsg(event.message)
|
||||
is SessionEvent.Image -> items + TranscriptItem.ImageItem(event.ref)
|
||||
is SessionEvent.Image ->
|
||||
// Under the call that produced it when there is one, and a row of
|
||||
// its own when there is not -- a person's own attachment belongs
|
||||
// to no call, and neither does one whose call fell outside the
|
||||
// loaded window.
|
||||
if (
|
||||
event.about != null &&
|
||||
items.any { it is TranscriptItem.ToolRun && it.id == event.about }
|
||||
) {
|
||||
updateTool(items, event.about) { it.copy(images = it.images + event.ref) }
|
||||
} else {
|
||||
items + TranscriptItem.ImageItem(event.ref)
|
||||
}
|
||||
is SessionEvent.Unknown -> items + TranscriptItem.Note("[${event.type}]")
|
||||
// Screen-level state, not transcript rows -- see SessionScreen.
|
||||
is SessionEvent.UsageDelta -> items
|
||||
@@ -608,6 +627,7 @@ fun SessionScreen(
|
||||
act { answerQuestion(settings, summary.id, ask.id, answer) }
|
||||
}
|
||||
},
|
||||
image = { ref -> SessionImage(settings, summary.id, ref) },
|
||||
)
|
||||
is TranscriptRow.Single ->
|
||||
when (val item = row.item) {
|
||||
@@ -635,6 +655,7 @@ fun SessionScreen(
|
||||
}
|
||||
}
|
||||
},
|
||||
image = { ref -> SessionImage(settings, summary.id, ref) },
|
||||
)
|
||||
is TranscriptItem.QuestionCard ->
|
||||
QuestionRow(item) { answer ->
|
||||
|
||||
Reference in new issue
Block a user