Select any of the transcript, and take a queued message back

Two things a reader could not do to what is on screen.

**Selection.** Nothing in the transcript was selectable at all, so a
command, a path or an error message could be read and not copied. One
`SelectionContainer` around the whole list rather than one per row: a
transcript is one body of text to a reader, and a selection has to be able
to run from a reply into the tool output under it. Per row it also could
not, and whatever was drawn without a container would have been silently
unselectable -- a state nothing on screen reports. Rows keep their tap
handlers; checked on the emulator that expanding a tool call, scrolling and
flinging are all unaffected, since a selection is a long press.

**Taking a message back.** A message sent into a running turn sits as a
bubble waiting to be read, and there was no way to change your mind: it is
tappable now, and the server answers `POST /sessions/{id}/unqueue`.

The answer has three states, and the middle one is the point. Claude's
driver writes a steer into the CLI's stdin the instant it arrives -- that
is what makes it reach the model at the next tool boundary rather than at
the end of the turn, and it was measured -- so the line is already gone and
`AlreadySent` is the only honest answer it can give. Holding the write
until a boundary would make the drop real and cost a steer one model call,
which is the latency the immediate write exists to remove; rejected on that
trade, with the reasoning in PLAN.md. The refusal is drawn on the bubble
that was pressed rather than in the error row under the header, a screen
away from it.

Where a driver really does hold its queue -- echo today -- the message goes
for good, and it goes as an `Event::MessageDropped` rather than as a return
value: every device watching the session loses the bubble, and a phone that
reconnects and replays the `messageQueued` does not put back one that was
cancelled with nothing left to resolve it.
This commit is contained in:
iris committed 2026-08-31 22:20:47 -04:00
1 parent 778b2e3b04
commit 82401cd887
12 files changed
+439 -48

No files matched your search

@@ -492,6 +492,23 @@ fun sendMessage(
) {}
}
/**
* Takes back a message the session has not read yet, named by the id its `messageQueued` carried.
*
* Throws rather than returning an outcome, because both ways of failing are things the reader has
* to be told: 409 means the session was already given it, and 404 means nothing is waiting under
* that id. The bubble disappearing is the success case and it arrives on the event stream, not from
* here -- every device drops it, not only the one that tapped.
*/
fun unqueueMessage(settings: ServerSettings, sessionId: String, messageId: String) {
requestFromServer(
settings,
"/sessions/$sessionId/unqueue",
method = "POST",
jsonBody = JSONObject().put("messageId", messageId).toString(),
) {}
}
/** Uploads one picked image; the returned id goes into [sendMessage]. */
fun uploadAttachment(
settings: ServerSettings,