Keep open tool cards out of groups
This commit is contained in:
1 parent
33b130b6bb
commit
9bcf0f1a48
4 files changed
+55
-48
No files matched your search
@@ -1129,17 +1129,17 @@ dev-updater (Kotlin 2.4.x, CMP 1.11.x, JDK 21).
|
||||
4. **Session screen** — the core:
|
||||
- The transcript rendered from the event stream: markdown, inline images,
|
||||
tool cards, question cards.
|
||||
- **A run of adjacent tool calls is one collapsed card, except for the
|
||||
call still running and the last call in the transcript** (2026-09-15).
|
||||
What the session is doing right now, or did last, is the one thing worth
|
||||
seeing without opening anything, and a heading counting it hides it. What
|
||||
folds a call back into its run is not finishing but being *overtaken*:
|
||||
anything arriving behind it, a reply included, makes it history, and a
|
||||
session that has run its last command and is composing its answer leaves
|
||||
that command standing until the answer starts. Grouping is 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.
|
||||
- **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).
|
||||
What the session is doing right now, did last, or what the reader is
|
||||
looking at is the one thing worth seeing without a heading hiding it.
|
||||
What folds a finished call back into its run is being *overtaken* while
|
||||
closed: anything arriving behind it, a reply included, makes it history,
|
||||
and a session that has run its last command and is composing its answer
|
||||
leaves that command standing until the answer starts. Grouping is 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
|
||||
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
|
||||
|
||||
@@ -307,8 +307,6 @@ fun SessionScreen(
|
||||
// Which runs of adjacent tool calls are open, by the group row's own key, so a group survives
|
||||
// more calls arriving after it.
|
||||
var expandedGroups by remember { mutableStateOf(setOf<String>()) }
|
||||
// Calls already drawn inside a group, so being folded into one is noticed exactly once each.
|
||||
var everGrouped by remember { mutableStateOf(setOf<String>()) }
|
||||
// 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.
|
||||
var expandedNotes by remember { mutableStateOf(setOf<Long>()) }
|
||||
@@ -412,7 +410,7 @@ fun SessionScreen(
|
||||
// arrived, waiting for them to return to the newest end. See [record].
|
||||
var held by remember { mutableStateOf(listOf<SeqEvent>()) }
|
||||
// What is actually drawn: the transcript with runs of adjacent tool calls folded into one row.
|
||||
val rows = remember(items) { groupToolRuns(items) }
|
||||
val rows = remember(items, expandedTools) { groupToolRuns(items, expandedTools) }
|
||||
// Bumped when a cold reply's parses become ready, so the flatten runs again and can split it.
|
||||
var warmedTick by remember { mutableIntStateOf(0) }
|
||||
val units =
|
||||
@@ -609,7 +607,7 @@ fun SessionScreen(
|
||||
* Computed from `items` rather than `rows` for the reason [loadOlderPage] gives.
|
||||
*/
|
||||
fun anchorRow(seq: Long): Long? {
|
||||
val ordered = groupToolRuns(items)
|
||||
val ordered = groupToolRuns(items, expandedTools)
|
||||
val at = ordered.indexOfLast { it.startSeq <= seq }
|
||||
// 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
|
||||
@@ -702,25 +700,6 @@ fun SessionScreen(
|
||||
}
|
||||
}
|
||||
|
||||
// A call opened on its own stays open when it is folded into a group -- either because a second
|
||||
// call in the same run arrived, or because something landed behind the last call and it
|
||||
// rejoined the run it was standing outside of. Until this, watching a Bash call and having the
|
||||
// session make another one shut the one being read and folded it behind "Called 2 tools".
|
||||
//
|
||||
// Per call rather than per group, because a group outlives the calls joining it: considered at
|
||||
// the moment each call first lands inside one, and never again, so the reader who then shuts
|
||||
// the group has shut it.
|
||||
LaunchedEffect(rows) {
|
||||
val fresh =
|
||||
rows.filterIsInstance<TranscriptRow.Tools>().flatMap { group ->
|
||||
group.calls.filter { it.id !in everGrouped }.map { group.key to it.id }
|
||||
}
|
||||
if (fresh.isEmpty()) return@LaunchedEffect
|
||||
expandedGroups =
|
||||
expandedGroups + fresh.filter { it.second in expandedTools }.map { it.first }
|
||||
everGrouped = everGrouped + fresh.map { it.second }
|
||||
}
|
||||
|
||||
// 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
|
||||
// anybody here can measure: how long it has been going. A bar filling up would be this screen
|
||||
|
||||
@@ -97,15 +97,20 @@ sealed class TranscriptRow {
|
||||
* 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.
|
||||
*
|
||||
* The last call is left alone too, and so is one still running wherever in its run it sits. What
|
||||
* the session is doing, or did last, is the one thing worth seeing without opening anything, and a
|
||||
* heading counting it hides it. What folds a call back into its run is therefore not finishing but
|
||||
* being overtaken: anything arriving behind it, a reply included, makes it history.
|
||||
* The last call is left alone too, as is one still running or one the reader has open, wherever in
|
||||
* its run it sits. What the session is doing, did last, or is being read is the one thing worth
|
||||
* seeing without a heading hiding it. A finished call therefore folds back into its run only once
|
||||
* it has been overtaken and is closed.
|
||||
*/
|
||||
fun groupToolRuns(items: List<TranscriptItem>): List<TranscriptRow> =
|
||||
DebugStats.timed("grouped tool runs") { groupRuns(items) }
|
||||
fun groupToolRuns(
|
||||
items: List<TranscriptItem>,
|
||||
expandedTools: Set<String> = emptySet(),
|
||||
): List<TranscriptRow> = DebugStats.timed("grouped tool runs") { groupRuns(items, expandedTools) }
|
||||
|
||||
private fun groupRuns(items: List<TranscriptItem>): List<TranscriptRow> {
|
||||
private fun groupRuns(
|
||||
items: List<TranscriptItem>,
|
||||
expandedTools: Set<String>,
|
||||
): List<TranscriptRow> {
|
||||
val rows = mutableListOf<TranscriptRow>()
|
||||
var run = mutableListOf<TranscriptItem.ToolRun>()
|
||||
// The run being walked, and how many rows it has produced so far -- which is what decides the
|
||||
@@ -143,9 +148,9 @@ private fun groupRuns(items: List<TranscriptItem>): List<TranscriptRow> {
|
||||
when {
|
||||
call == null -> rows += TranscriptRow.Single(item)
|
||||
// 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 the last
|
||||
// thing that happened and part of its group once a reply lands behind it.
|
||||
call.done && index != items.lastIndex -> run += call
|
||||
// recorded on the call: the same finished call is a row of its own while it is last or
|
||||
// open, and part of its group once something follows it and the reader closes it.
|
||||
call.done && call.id !in expandedTools && index != items.lastIndex -> run += call
|
||||
else -> {
|
||||
flush()
|
||||
run += call
|
||||
|
||||
@@ -5,10 +5,9 @@ import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
/**
|
||||
* How a run of tool calls is cut into rows: the call still running and the last call in the
|
||||
* transcript are drawn on their own, and every 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.
|
||||
* How a run of tool calls is cut into rows: a call still running, open, or last in the transcript
|
||||
* is drawn on its own, and every 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 {
|
||||
private var seq = 0L
|
||||
@@ -75,6 +74,30 @@ class ToolRowsTest {
|
||||
assertKeysDistinct(rows)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun an_open_call_stays_out_of_its_group_until_it_is_closed() {
|
||||
val calls =
|
||||
listOf(
|
||||
call("a"),
|
||||
call("b", runId = "a"),
|
||||
call("c", runId = "a"),
|
||||
call("d", runId = "a"),
|
||||
reply(),
|
||||
)
|
||||
|
||||
val whileOpen = groupToolRuns(calls, expandedTools = setOf("b"))
|
||||
val afterItCloses = groupToolRuns(calls)
|
||||
|
||||
assertEquals(
|
||||
listOf(listOf("a"), listOf("b"), listOf("c", "d"), listOf("reply")),
|
||||
shape(whileOpen),
|
||||
)
|
||||
assertTrue(whileOpen[1] is TranscriptRow.Single, "$whileOpen")
|
||||
assertKeysDistinct(whileOpen)
|
||||
assertEquals(listOf(listOf("a", "b", "c", "d"), listOf("reply")), shape(afterItCloses))
|
||||
assertTrue(afterItCloses.first() is TranscriptRow.Tools, "$afterItCloses")
|
||||
}
|
||||
|
||||
/**
|
||||
* The one case where the run's name is a call that is not in the run's first row: a page of
|
||||
* history joined onto a run whose own first call is still going ([joinPages] renames the older
|
||||
|
||||
Reference in new issue
Block a user