Keep tool group keys unique across transcript

This commit is contained in:
iris-ai committed 2026-09-15 23:20:47 -04:00
1 parent f00a178cf0
commit 947ea8ecf2
2 files changed
+35 -12

No files matched your search

@@ -113,24 +113,27 @@ private fun groupRuns(
): List<TranscriptRow> { ): List<TranscriptRow> {
val rows = mutableListOf<TranscriptRow>() val rows = mutableListOf<TranscriptRow>()
var run = mutableListOf<TranscriptItem.ToolRun>() var run = mutableListOf<TranscriptItem.ToolRun>()
// The run being walked, and how many rows it has produced so far -- which is what decides the // A run can occupy more than one non-adjacent piece, so claimed keys span the whole transcript
// keys below. // rather than resetting at each piece.
var runId: String? = null var runId: String? = null
var emitted = 0 val claimedKeys = mutableSetOf<String>()
fun flush() { fun flush() {
val first = run.firstOrNull() ?: return val first = run.firstOrNull() ?: return
// The first row a run produces keeps the run's name, which is the name that survives a page // The first piece keeps the run's name, which survives a page landing in front of it
// of history landing in front of it ([adoptRun]); losing it is the transcript stepping // ([adoptRun]). Later pieces qualify that name with their first call; the suffix is the
// under whoever is reading. The pieces a call standing on its own cuts off the back of the // final guard because a duplicate LazyColumn key takes down the whole screen.
// run have no such name, so each takes its own first call's id, which is unique because a var key = first.runId
// call id is. The run's name goes in front of it because the two can otherwise be the same if (!claimedKeys.add(key)) {
// string: the call a run was named after can itself be the one standing on its own. key = "${first.runId}/${first.id}"
val key = if (emitted == 0) first.runId else "${first.runId}/${first.id}" var suffix = 2
while (!claimedKeys.add(key)) {
key = "${first.runId}/${first.id}/${suffix++}"
}
}
rows += rows +=
if (run.size == 1) TranscriptRow.Single(first, key) if (run.size == 1) TranscriptRow.Single(first, key)
else TranscriptRow.Tools(run.toList(), key) else TranscriptRow.Tools(run.toList(), key)
emitted++
run = mutableListOf() run = mutableListOf()
} }
@@ -143,7 +146,6 @@ private fun groupRuns(
if (call == null || call.runId != runId) { if (call == null || call.runId != runId) {
flush() flush()
runId = call?.runId runId = call?.runId
emitted = 0
} }
when { when {
call == null -> rows += TranscriptRow.Single(item) call == null -> rows += TranscriptRow.Single(item)
@@ -111,6 +111,27 @@ class ToolRowsTest {
assertEquals("b", rows.first().key) assertEquals("b", rows.first().key)
} }
@Test
fun a_run_that_reappears_after_another_row_keeps_distinct_keys() {
val rows =
groupToolRuns(
listOf(
call("older", runId = "exec-1"),
call("older-2", runId = "exec-1"),
reply(),
call("exec-1", runId = "exec-1"),
call("newer", runId = "exec-1"),
reply(),
)
)
assertTrue(rows[0] is TranscriptRow.Tools, "$rows")
assertTrue(rows[2] is TranscriptRow.Tools, "$rows")
assertKeysDistinct(rows)
assertEquals("exec-1", rows[0].key)
assertEquals("exec-1/exec-1", rows[2].key)
}
/** /**
* Finishing is not what folds a call back in -- being overtaken is. A session that has run its * Finishing is not what folds a call back in -- being overtaken is. A session that has run its
* last command and is writing its reply leaves that command standing until the reply starts. * last command and is writing its reply leaves that command standing until the reply starts.