Draw a peer message above the turn it started, not below it
The live Claude Code path only learns a turn was another agent's when the turn ends -- the whole of the message arrives as an `origin` object on the `result` -- so the note was appended after everything it caused, and the transcript showed the answer above the question. It cannot be recorded in place: by the time anyone knows, the reply is already written, and the transcript is append-only. So the event carries where it belongs instead. `PeerMessage` gains `turnStart`, the seq of the status that opened its turn, stamped by the pump -- the only thing that knows a seq and the only thing that sees every driver's turns. The phone gives the note that seq, so it sorts into place rather than being drawn out of order at the end. A status draws no row, so there is nothing for it to collide with and the list stays sorted, which the scroll anchor and paging both depend on. Absent where there is nothing to correct: a message replayed out of a session file by `import` is already in the right place, and one that opened no turn has no turn to sit above. Both stay where they arrive. The echo driver gets `/peer-turn` for the live shape, beside `/peer` for the in-place one. Verified on the emulator both ways, live and on replay, plus an ordinary `/tools` turn to confirm the run grouping the insertion cuts through is unaffected. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7525fc925a
commit
465645cefb
9 files changed
+246
-13
No files matched your search
@@ -268,6 +268,56 @@ private fun adoptRun(
|
||||
tail.map { (it as TranscriptItem.ToolRun).copy(runId = joining) }
|
||||
}
|
||||
|
||||
/**
|
||||
* A peer message goes above the turn it started, not where it happened to arrive.
|
||||
*
|
||||
* The live Claude Code path cannot record it in place: the CLI says nothing about a peer message
|
||||
* until the turn's `result`, so the event lands below the whole reply it caused -- the answer
|
||||
* printed above the question. The server stamps it with where that turn began
|
||||
* ([SessionEvent.PeerMessage.turnStart]) and the note takes that seq, so it sorts into the list
|
||||
* where it belongs rather than being drawn out of order at the end.
|
||||
*
|
||||
* Taking the turn's opening seq as its own is also what keeps the list sorted, which anchors and
|
||||
* paging both depend on. That seq belongs to a status change, and a status draws no row, so there
|
||||
* is nothing for it to collide with.
|
||||
*
|
||||
* Without a stamp -- a message replayed out of a session file, which is already in the right place
|
||||
* -- it stays where it arrived.
|
||||
*/
|
||||
private fun placePeerNote(
|
||||
items: List<TranscriptItem>,
|
||||
seq: Long,
|
||||
event: SessionEvent.PeerMessage,
|
||||
): List<TranscriptItem> {
|
||||
val at = event.turnStart ?: return items + TranscriptItem.PeerNote(seq, event.from, event.text)
|
||||
val note = TranscriptItem.PeerNote(at, event.from, event.text)
|
||||
val index = items.indexOfFirst { it.seq > at }
|
||||
if (index < 0) return items + note
|
||||
val behind = (items.getOrNull(index - 1) as? TranscriptItem.ToolRun)?.runId
|
||||
return items.subList(0, index) + note + splitRun(items.subList(index, items.size), behind)
|
||||
}
|
||||
|
||||
/**
|
||||
* The calls the note now sits in front of, renamed if they were sharing a run with the calls behind
|
||||
* it.
|
||||
*
|
||||
* A run is named from what a call landed next to (see [runIdFor]), and nothing there knows about
|
||||
* turns -- so a turn opening with a tool call, straight after one that ended with one, folds them
|
||||
* into a single run. Left alone, [groupToolRuns] would flush at the note and hand both halves the
|
||||
* same name: two rows with one key, which a keyed list cannot draw at all.
|
||||
*
|
||||
* The later half is the one renamed, which is the opposite of a page join ([adoptRun]) and right
|
||||
* for the opposite reason. There the two halves were always one run and the newer was already on
|
||||
* screen; here they were never one turn's work, and both halves change appearance at the same
|
||||
* moment the note appears between them.
|
||||
*/
|
||||
private fun splitRun(tail: List<TranscriptItem>, behind: String?): List<TranscriptItem> {
|
||||
val first = tail.firstOrNull() as? TranscriptItem.ToolRun ?: return tail
|
||||
if (behind == null || first.runId != behind) return tail
|
||||
val run = tail.takeWhile { it is TranscriptItem.ToolRun && it.runId == behind }
|
||||
return run.map { (it as TranscriptItem.ToolRun).copy(runId = first.id) } + tail.drop(run.size)
|
||||
}
|
||||
|
||||
fun foldEvent(items: List<TranscriptItem>, entry: SeqEvent): List<TranscriptItem> =
|
||||
when (val event = entry.event) {
|
||||
is SessionEvent.UserMessage ->
|
||||
@@ -361,8 +411,7 @@ fun foldEvent(items: List<TranscriptItem>, entry: SeqEvent): List<TranscriptItem
|
||||
else -> it
|
||||
}
|
||||
}
|
||||
is SessionEvent.PeerMessage ->
|
||||
items + TranscriptItem.PeerNote(entry.seq, event.from, event.text)
|
||||
is SessionEvent.PeerMessage -> placePeerNote(items, entry.seq, event)
|
||||
is SessionEvent.CommandSent -> items + TranscriptItem.CommandRow(entry.seq, event.text)
|
||||
// Screen-level state, not transcript rows -- see SessionScreen.
|
||||
is SessionEvent.CommandQueued -> items
|
||||
|
||||
Reference in new issue
Block a user