Hold an open call out of its run without taking one out of a group
Being open did two things to grouping, and only one of them was wanted. It held a call standing on its own out of the run it belongs to, so a command finishing behind the card being read no longer shuts it and folds it away mid-sentence. It also took a call *out* of the group it was already inside, and that is what made collapsing jump: grouping is what gives a row its identity, so one tap rebuilt the rows around the finger -- opening a call inside a group split the group into two pieces with mismatched keys, and closing one replaced three rows with one, which no anchor survives. Measured at 450px of jump, with the card that was closed going with it. So the held-out set is now the screen's, not the transcript's: a call that has never been drawn inside a group and is open stands out of its run, and a call that has been in one stays in it whatever the reader does to it. Being inside a group once is a fact about what the reader has been shown, which is why the screen is what remembers it. Checked with ktfmtFormat, compileDebugKotlin, lintDebug and testDebugUnitTest, and on the emulator against the sandbox: opening a call inside an open group of six leaves it one group of six and closing it returns every row to the pixel it came from; a call opened while standing alone survives a reply landing behind it, and folds back into "Called 3 tools" when it is closed without moving the rows below it.
This commit is contained in:
1 parent
463acb28fa
commit
b86a5dc37a
4 files changed
+73
-36
No files matched your search
@@ -1157,16 +1157,27 @@ dev-updater (Kotlin 2.4.x, CMP 1.11.x, JDK 21).
|
|||||||
- The transcript rendered from the event stream: markdown, inline images,
|
- The transcript rendered from the event stream: markdown, inline images,
|
||||||
tool cards, question cards.
|
tool cards, question cards.
|
||||||
- **A run of adjacent tool calls is one collapsed card, except for a call
|
- **A run of adjacent tool calls is one collapsed card, except for a call
|
||||||
that is still running, open, or last in the transcript** (2026-09-15).
|
that is still running, last in the transcript, or held out of its run by
|
||||||
What the session is doing right now, did last, or what the reader is
|
being read** (2026-09-15, corrected 2026-09-16). What the session is
|
||||||
looking at is the one thing worth seeing without a heading hiding it.
|
doing right now, or did last, is the one thing worth seeing without a
|
||||||
What folds a finished call back into its run is being *overtaken* while
|
heading hiding it. What folds a finished call back into its run is being
|
||||||
closed: anything arriving behind it, a reply included, makes it history,
|
*overtaken*: anything arriving behind it, a reply included, makes it
|
||||||
and a session that has run its last command and is composing its answer
|
history — except while somebody has it open, since a card being read is
|
||||||
leaves that command standing until the answer starts. Grouping is a
|
not history to them, and a command finishing behind it used to shut it
|
||||||
display decision (`groupToolRuns`) and a cut run's pieces are keyed there
|
and fold it away mid-sentence.
|
||||||
— the first piece keeps the run's name, since that name is what survives
|
**Being open only ever holds a call out; it never takes one back out of
|
||||||
a page of history landing in front of it.
|
a group it is already in.** That was tried for a day and is what
|
||||||
|
"collapsing jumps" was: grouping gives a row its identity, so a rule
|
||||||
|
reading the open set both ways let one tap rebuild the rows around the
|
||||||
|
finger — closing a call replaced three rows with one, and no anchor
|
||||||
|
survives a row that has ceased to exist. A call inside an open group is
|
||||||
|
visible where it is and has nothing to gain by moving. The screen is
|
||||||
|
what remembers which calls have been in a group (`everGrouped` feeding
|
||||||
|
`heldOut`), because that is a fact about what the reader has been shown
|
||||||
|
rather than about the transcript. Grouping is otherwise a display
|
||||||
|
decision (`groupToolRuns`) and a cut run's pieces are keyed there — the
|
||||||
|
first piece keeps the run's name, since that name is what survives a
|
||||||
|
page of history landing in front of it.
|
||||||
- **Anything that is a note *about* the conversation rather than a turn in
|
- **Anything that is a note *about* the conversation rather than a turn in
|
||||||
it is closed by default** — a tool call, a peer message, a memory note.
|
it is closed by default** — a tool call, a peer message, a memory note.
|
||||||
Open-ness is the screen's, never the card's: a card that remembered for
|
Open-ness is the screen's, never the card's: a card that remembered for
|
||||||
|
|||||||
@@ -319,6 +319,11 @@ fun SessionScreen(
|
|||||||
// Which runs of adjacent tool calls are open, by the group row's own key, so a group survives
|
// Which runs of adjacent tool calls are open, by the group row's own key, so a group survives
|
||||||
// more calls arriving after it.
|
// more calls arriving after it.
|
||||||
var expandedGroups by remember { mutableStateOf(setOf<String>()) }
|
var expandedGroups by remember { mutableStateOf(setOf<String>()) }
|
||||||
|
// Calls that have been drawn inside a group. A call that never has and is open is one the
|
||||||
|
// reader pulled up while it stood on its own, and it keeps standing there until they are done
|
||||||
|
// with it; a call already in a group is visible inside it, and taking it out to open it would
|
||||||
|
// rebuild the rows around the reader's finger. See [groupToolRuns].
|
||||||
|
var everGrouped by remember { mutableStateOf(setOf<String>()) }
|
||||||
// Which messages from other agents are open, by the seq that identifies their row. Closed by
|
// Which messages from other agents are open, by the seq that identifies their row. Closed by
|
||||||
// default, which is the rule for anything new in this transcript.
|
// default, which is the rule for anything new in this transcript.
|
||||||
var expandedNotes by remember { mutableStateOf(setOf<Long>()) }
|
var expandedNotes by remember { mutableStateOf(setOf<Long>()) }
|
||||||
@@ -422,7 +427,8 @@ fun SessionScreen(
|
|||||||
// arrived, waiting for them to return to the newest end. See [record].
|
// arrived, waiting for them to return to the newest end. See [record].
|
||||||
var held by remember { mutableStateOf(listOf<SeqEvent>()) }
|
var held by remember { mutableStateOf(listOf<SeqEvent>()) }
|
||||||
// What is actually drawn: the transcript with runs of adjacent tool calls folded into one row.
|
// What is actually drawn: the transcript with runs of adjacent tool calls folded into one row.
|
||||||
val rows = remember(items, expandedTools) { groupToolRuns(items, expandedTools) }
|
val heldOut = expandedTools - everGrouped
|
||||||
|
val rows = remember(items, heldOut) { groupToolRuns(items, heldOut) }
|
||||||
// Bumped when a cold reply's parses become ready, so the flatten runs again and can split it.
|
// Bumped when a cold reply's parses become ready, so the flatten runs again and can split it.
|
||||||
var warmedTick by remember { mutableIntStateOf(0) }
|
var warmedTick by remember { mutableIntStateOf(0) }
|
||||||
val units =
|
val units =
|
||||||
@@ -634,7 +640,7 @@ fun SessionScreen(
|
|||||||
* Computed from `items` rather than `rows` for the reason [loadOlderPage] gives.
|
* Computed from `items` rather than `rows` for the reason [loadOlderPage] gives.
|
||||||
*/
|
*/
|
||||||
fun anchorRow(seq: Long): Long? {
|
fun anchorRow(seq: Long): Long? {
|
||||||
val ordered = groupToolRuns(items, expandedTools)
|
val ordered = groupToolRuns(items, heldOut)
|
||||||
val at = ordered.indexOfLast { it.startSeq <= seq }
|
val at = ordered.indexOfLast { it.startSeq <= seq }
|
||||||
// Zero is the oldest loaded row, which is the half-row above; not found is -1.
|
// Zero is the oldest loaded row, which is the half-row above; not found is -1.
|
||||||
return if (at > 0) ordered[at].startSeq else null
|
return if (at > 0) ordered[at].startSeq else null
|
||||||
@@ -727,6 +733,17 @@ fun SessionScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Which calls have been inside a group, which is what [heldOut] subtracts: a call the reader
|
||||||
|
// opened while it stood on its own is held out of the run it belongs to until they close it,
|
||||||
|
// and a call that has been in a group is one that opening will never take back out.
|
||||||
|
LaunchedEffect(rows) {
|
||||||
|
val fresh =
|
||||||
|
rows.filterIsInstance<TranscriptRow.Tools>().flatMap { group ->
|
||||||
|
group.calls.map { it.id }.filter { it !in everGrouped }
|
||||||
|
}
|
||||||
|
if (fresh.isNotEmpty()) everGrouped = everGrouped + fresh
|
||||||
|
}
|
||||||
|
|
||||||
// A compaction reports nothing about its own progress -- measured against the CLI, which says
|
// A compaction reports nothing about its own progress -- measured against the CLI, which says
|
||||||
// it has started and then nothing at all until it is done. So what this counts is the one thing
|
// it has started and then nothing at all until it is done. So what this counts is the one thing
|
||||||
// anybody here can measure: how long it has been going. A bar filling up would be this screen
|
// anybody here can measure: how long it has been going. A bar filling up would be this screen
|
||||||
|
|||||||
@@ -97,20 +97,26 @@ sealed class TranscriptRow {
|
|||||||
* A single call is left alone: "Called 1 tool" hides a card to say the same thing in more words,
|
* A single call is left alone: "Called 1 tool" hides a card to say the same thing in more words,
|
||||||
* and the run this exists for is the burst of five greps nobody wants to scroll past.
|
* and the run this exists for is the burst of five greps nobody wants to scroll past.
|
||||||
*
|
*
|
||||||
* The last call is left alone too, as is one still running or one the reader has open, wherever in
|
* The last call is left alone too, and so is one still running wherever in its run it sits. What
|
||||||
* its run it sits. What the session is doing, did last, or is being read is the one thing worth
|
* the session is doing, or did last, is the one thing worth seeing without opening anything, and a
|
||||||
* seeing without a heading hiding it. A finished call therefore folds back into its run only once
|
* heading counting it hides it. What folds a call back into its run is therefore not finishing but
|
||||||
* it has been overtaken and is closed.
|
* being overtaken: anything arriving behind it, a reply included, makes it history.
|
||||||
|
*
|
||||||
|
* [heldOut] is the one thing being read can change, and only in that direction: a call standing on
|
||||||
|
* its own that somebody is reading is not overtaken while they read it. Opening a call *already*
|
||||||
|
* inside a group does not pull it out (2026-09-16, after it briefly did) -- it is visible where it
|
||||||
|
* is, and grouping is what gives a row its identity, so a rule that reads the open set both ways
|
||||||
|
* makes the reader's own tap rebuild the rows around it: three rows became one the moment a call
|
||||||
|
* was closed, and no anchor survives a row that no longer exists -- the list jumped by 450px and
|
||||||
|
* took the closed card with it. Which calls are held out is [SessionScreen]'s to say, since being
|
||||||
|
* inside a group once is what settles it.
|
||||||
*/
|
*/
|
||||||
fun groupToolRuns(
|
fun groupToolRuns(
|
||||||
items: List<TranscriptItem>,
|
items: List<TranscriptItem>,
|
||||||
expandedTools: Set<String> = emptySet(),
|
heldOut: Set<String> = emptySet(),
|
||||||
): List<TranscriptRow> = DebugStats.timed("grouped tool runs") { groupRuns(items, expandedTools) }
|
): List<TranscriptRow> = DebugStats.timed("grouped tool runs") { groupRuns(items, heldOut) }
|
||||||
|
|
||||||
private fun groupRuns(
|
private fun groupRuns(items: List<TranscriptItem>, heldOut: Set<String>): List<TranscriptRow> {
|
||||||
items: List<TranscriptItem>,
|
|
||||||
expandedTools: Set<String>,
|
|
||||||
): List<TranscriptRow> {
|
|
||||||
val rows = mutableListOf<TranscriptRow>()
|
val rows = mutableListOf<TranscriptRow>()
|
||||||
var run = mutableListOf<TranscriptItem.ToolRun>()
|
var run = mutableListOf<TranscriptItem.ToolRun>()
|
||||||
// A run can occupy more than one non-adjacent piece, so claimed keys span the whole transcript
|
// A run can occupy more than one non-adjacent piece, so claimed keys span the whole transcript
|
||||||
@@ -150,9 +156,10 @@ private fun groupRuns(
|
|||||||
when {
|
when {
|
||||||
call == null -> rows += TranscriptRow.Single(item)
|
call == null -> rows += TranscriptRow.Single(item)
|
||||||
// Standing outside the run is the call's place in the list as it is now, not something
|
// Standing outside the run is the call's place in the list as it is now, not something
|
||||||
// recorded on the call: the same finished call is a row of its own while it is last or
|
// recorded on the call: the same finished call is a row of its own while it is the last
|
||||||
// open, and part of its group once something follows it and the reader closes it.
|
// thing that happened, or open and never yet grouped, and part of its group once a
|
||||||
call.done && call.id !in expandedTools && index != items.lastIndex -> run += call
|
// reply lands behind it.
|
||||||
|
call.done && call.id !in heldOut && index != items.lastIndex -> run += call
|
||||||
else -> {
|
else -> {
|
||||||
flush()
|
flush()
|
||||||
run += call
|
run += call
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ import kotlin.test.assertEquals
|
|||||||
import kotlin.test.assertTrue
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* How a run of tool calls is cut into rows: a call still running, open, or last in the transcript
|
* How a run of tool calls is cut into rows: the call still running, the last call in the
|
||||||
* is drawn on its own, and every piece the cut leaves behind still has a key of its own -- two rows
|
* transcript, and one held out because the reader has it open are drawn on their own, and every
|
||||||
* sharing one key take the app down, and a key that moves takes the reader's place with it.
|
* piece the cut leaves behind still has a key of its own -- two rows sharing one key take the app
|
||||||
|
* down, and a key that moves takes the reader's place with it.
|
||||||
*/
|
*/
|
||||||
class ToolRowsTest {
|
class ToolRowsTest {
|
||||||
private var seq = 0L
|
private var seq = 0L
|
||||||
@@ -74,8 +75,12 @@ class ToolRowsTest {
|
|||||||
assertKeysDistinct(rows)
|
assertKeysDistinct(rows)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A call held out is one the reader opened while it stood on its own; being overtaken while
|
||||||
|
* they read it does not fold it away, and closing it hands it back to its run.
|
||||||
|
*/
|
||||||
@Test
|
@Test
|
||||||
fun an_open_call_stays_out_of_its_group_until_it_is_closed() {
|
fun a_held_out_call_stays_out_of_its_group() {
|
||||||
val calls =
|
val calls =
|
||||||
listOf(
|
listOf(
|
||||||
call("a"),
|
call("a"),
|
||||||
@@ -85,15 +90,12 @@ class ToolRowsTest {
|
|||||||
reply(),
|
reply(),
|
||||||
)
|
)
|
||||||
|
|
||||||
val whileOpen = groupToolRuns(calls, expandedTools = setOf("b"))
|
val whileHeld = groupToolRuns(calls, heldOut = setOf("d"))
|
||||||
val afterItCloses = groupToolRuns(calls)
|
val afterItCloses = groupToolRuns(calls)
|
||||||
|
|
||||||
assertEquals(
|
assertEquals(listOf(listOf("a", "b", "c"), listOf("d"), listOf("reply")), shape(whileHeld))
|
||||||
listOf(listOf("a"), listOf("b"), listOf("c", "d"), listOf("reply")),
|
assertTrue(whileHeld[1] is TranscriptRow.Single, "$whileHeld")
|
||||||
shape(whileOpen),
|
assertKeysDistinct(whileHeld)
|
||||||
)
|
|
||||||
assertTrue(whileOpen[1] is TranscriptRow.Single, "$whileOpen")
|
|
||||||
assertKeysDistinct(whileOpen)
|
|
||||||
assertEquals(listOf(listOf("a", "b", "c", "d"), listOf("reply")), shape(afterItCloses))
|
assertEquals(listOf(listOf("a", "b", "c", "d"), listOf("reply")), shape(afterItCloses))
|
||||||
assertTrue(afterItCloses.first() is TranscriptRow.Tools, "$afterItCloses")
|
assertTrue(afterItCloses.first() is TranscriptRow.Tools, "$afterItCloses")
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user