Never run two turns into one, and say when a session waits on its own work
A turn started by something with no row of its own -- a subagent reporting back, a peer message the CLI only owns up to at the end -- met the previous reply with nothing between it, and the fold grew that reply rather than starting a new one. Two answers were drawn as one paragraph, running together mid-sentence with not even a space between them. The fold now refuses to grow a settled reply, and `joinPages` carries the same rule across a page boundary. The other half is the row. `Event::TaskNote` records a background task reporting back -- a subagent that finished, or a backgrounded command -- with its title, how it ended and what it said; `TaskNoteRow` draws it as a card, since somebody said this, and its own row rather than an update to the Task call's, which is above everything the session has said since. Reported once however many of the CLI's two lifecycle shapes arrive. `SessionStatus::Waiting` is a session whose own turn is over while work it started is not. `Idle` means "waiting for a person" and this means the opposite, so reporting it as idle sent a "finished" notification at the one moment that was untrue. Drawn as "waiting" in `waitingColor`; the queue and the held-command boundary release on either end-of-turn status, so a message sent while a subagent runs is not held until it finishes. And a usage limit the account hits inside a subagent now reaches the session as well as the subagent's transcript. `resume.rs` can only schedule against a session, and a background Task outliving its parent's turn is the ordinary case, so auto-resume was doing nothing at all for it. The status word and its colour were two `when`s on two screens, and the second missed `waiting` silently; they are `sessionStatusWord`/`sessionStatusColour` now. Echo's `/subagent n` reproduces the whole shape, staggered a second apart. Verified on the emulator against the sandbox: 169 server tests, ktfmt, clippy, rustfmt, Android lint and the JVM unit tests all clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
74c07d687a
commit
5711c2568a
17 files changed
+891
-85
No files matched your search
@@ -143,6 +143,26 @@ sealed class TranscriptItem {
|
||||
get() = arrived
|
||||
}
|
||||
|
||||
/**
|
||||
* A task the session started in the background reporting back: a subagent that finished, or a
|
||||
* backgrounded command.
|
||||
*
|
||||
* Its own row rather than an update to the Task call's, which is wherever the call was made --
|
||||
* above everything the session has said since, where a reader at the bottom would never see it
|
||||
* change. Here it is where it arrived, in front of the turn it caused.
|
||||
*/
|
||||
data class TaskNote(
|
||||
override val seq: Long,
|
||||
/** The tool call it belongs to; a subagent is named by that id. */
|
||||
val about: String,
|
||||
/**
|
||||
* The subagent's title, or null for a backgrounded command -- see [SessionEvent.TaskNote].
|
||||
*/
|
||||
val title: String?,
|
||||
val status: String,
|
||||
val summary: String?,
|
||||
) : TranscriptItem()
|
||||
|
||||
/**
|
||||
* A command the session ran on itself -- `/compact`, `/rename`. Kept in the transcript rather
|
||||
* than only shown while it waits, because it explains what follows: a conversation that
|
||||
@@ -261,9 +281,10 @@ fun joinPages(earlier: List<TranscriptItem>, later: List<TranscriptItem>): List<
|
||||
/**
|
||||
* Rejoins a message the page boundary cut, and hands back the two pages to concatenate.
|
||||
*
|
||||
* [foldEvent] never leaves two assistant messages next to each other inside one page, so two
|
||||
* meeting at a join are always the two halves of one reply, and leaving them apart drew a single
|
||||
* answer as two with a paragraph break through the middle of a sentence.
|
||||
* [foldEvent] never leaves an *unfinished* assistant message with another behind it inside one
|
||||
* page, so an unsettled one at a join is always the far half of the reply the boundary cut, and
|
||||
* leaving the two apart drew a single answer as two with a paragraph break through the middle of a
|
||||
* sentence. Two settled replies meeting there are two turns and stay two.
|
||||
*
|
||||
* The newer half keeps its identity, for the reason [adoptRun] gives. It grows by what the older
|
||||
* half brings, which is safe here and nowhere else -- the join is at the oldest end of what is
|
||||
@@ -278,6 +299,10 @@ private fun healSplitMessage(
|
||||
if (head !is TranscriptItem.AssistantMsg || tail !is TranscriptItem.AssistantMsg) {
|
||||
return earlier to later
|
||||
}
|
||||
// A settled reply is a whole turn, so the two are two answers that happen to meet at the
|
||||
// boundary rather than one cut in half -- the same distinction the fold makes, and joining them
|
||||
// here would put back exactly the run-together paragraph it stops.
|
||||
if (head.settled) return earlier to later
|
||||
return earlier.dropLast(1) to (listOf(tail.copy(text = head.text + tail.text)) + later.drop(1))
|
||||
}
|
||||
|
||||
@@ -364,9 +389,13 @@ fun foldEvent(items: List<TranscriptItem>, entry: SeqEvent): List<TranscriptItem
|
||||
// first of them: a row whose identity changed with every delta would be a new row on
|
||||
// every frame, and the list would jump for the whole of a streamed answer.
|
||||
val last = items.lastOrNull()
|
||||
if (last is TranscriptItem.AssistantMsg) {
|
||||
// A message growing again is not finished, whatever a status said in between.
|
||||
items.dropLast(1) + last.copy(text = last.text + event.delta, settled = false)
|
||||
// Only into a reply that is still arriving. A settled one is a turn that ended, and
|
||||
// text after it belongs to the next turn -- a separate message, drawn as its own row.
|
||||
// Growing it instead ran two answers together with not even a space between them,
|
||||
// which is what happens whenever a turn starts with nothing recorded in front of it:
|
||||
// a subagent reporting back, or a peer message the CLI only owns up to at the end.
|
||||
if (last is TranscriptItem.AssistantMsg && !last.settled) {
|
||||
items.dropLast(1) + last.copy(text = last.text + event.delta)
|
||||
} else {
|
||||
items + TranscriptItem.AssistantMsg(entry.seq, event.delta)
|
||||
}
|
||||
@@ -446,6 +475,15 @@ fun foldEvent(items: List<TranscriptItem>, entry: SeqEvent): List<TranscriptItem
|
||||
}
|
||||
}
|
||||
is SessionEvent.PeerMessage -> placePeerNote(items, entry.seq, event)
|
||||
is SessionEvent.TaskNote ->
|
||||
items +
|
||||
TranscriptItem.TaskNote(
|
||||
entry.seq,
|
||||
event.about,
|
||||
event.title,
|
||||
event.status,
|
||||
event.summary,
|
||||
)
|
||||
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