Merge remote-tracking branch 'origin/main'
This commit is contained in:
commit
5d47a1ec89
2 files changed
+73
-15
No files matched your search
@@ -93,6 +93,17 @@ sealed class TranscriptItem {
|
||||
data class ToolRun(
|
||||
override val seq: Long,
|
||||
val id: String,
|
||||
/**
|
||||
* The run of adjacent calls this one belongs to, named once when the call is folded in and
|
||||
* never recomputed.
|
||||
*
|
||||
* Carried rather than derived because a run can gain members at *either* end -- a new call
|
||||
* arriving beside it, or a page of history arriving in front of it -- so no function of its
|
||||
* current members is stable. It is the first call's id at the moment the run started, which
|
||||
* is a name rather than a description: [joinPages] hands it to older calls that turn out to
|
||||
* belong to the same run, instead of renaming the run they joined.
|
||||
*/
|
||||
val runId: String,
|
||||
val tool: String,
|
||||
val input: String,
|
||||
val output: String,
|
||||
@@ -177,6 +188,16 @@ sealed class TranscriptItem {
|
||||
) : TranscriptItem()
|
||||
}
|
||||
|
||||
/**
|
||||
* The run a call joins: the one it lands next to, or a new one named after itself.
|
||||
*
|
||||
* Only ever consulted when the call is first folded in. That is what makes the name stable -- a run
|
||||
* keeps whatever it was called when it started, however many calls arrive at either end of it
|
||||
* afterwards.
|
||||
*/
|
||||
private fun runIdFor(items: List<TranscriptItem>, id: String): String =
|
||||
(items.lastOrNull() as? TranscriptItem.ToolRun)?.runId ?: id
|
||||
|
||||
/**
|
||||
* Puts a page of older items in front of the ones already loaded, healing any tool call the page
|
||||
* boundary cut in two.
|
||||
@@ -220,7 +241,28 @@ fun joinPages(earlier: List<TranscriptItem>, later: List<TranscriptItem>): List<
|
||||
row
|
||||
}
|
||||
}
|
||||
return healed + later.filterNot { it is TranscriptItem.ToolRun && it.id in endedLater }
|
||||
val kept = later.filterNot { it is TranscriptItem.ToolRun && it.id in endedLater }
|
||||
return adoptRun(healed, kept) + kept
|
||||
}
|
||||
|
||||
/**
|
||||
* Hands the older calls at the join the name of the run they are joining.
|
||||
*
|
||||
* The two pages were folded separately, so a run split by the boundary came back as two runs with
|
||||
* two names. Naming the joined run after the *older* half would be the obvious way round and is the
|
||||
* wrong one: the newer half is the part already on screen, and renaming it is renaming the row the
|
||||
* reader is looking at, which is how a list loses its anchor and steps under them. So the arriving
|
||||
* calls take the name of the ones already there, and nothing visible changes identity.
|
||||
*/
|
||||
private fun adoptRun(
|
||||
earlier: List<TranscriptItem>,
|
||||
later: List<TranscriptItem>,
|
||||
): List<TranscriptItem> {
|
||||
val joining = (later.firstOrNull() as? TranscriptItem.ToolRun)?.runId ?: return earlier
|
||||
val tail = earlier.takeLastWhile { it is TranscriptItem.ToolRun }
|
||||
if (tail.isEmpty()) return earlier
|
||||
return earlier.dropLast(tail.size) +
|
||||
tail.map { (it as TranscriptItem.ToolRun).copy(runId = joining) }
|
||||
}
|
||||
|
||||
fun foldEvent(items: List<TranscriptItem>, entry: SeqEvent): List<TranscriptItem> =
|
||||
@@ -242,6 +284,7 @@ fun foldEvent(items: List<TranscriptItem>, entry: SeqEvent): List<TranscriptItem
|
||||
TranscriptItem.ToolRun(
|
||||
entry.seq,
|
||||
event.id,
|
||||
runIdFor(items, event.id),
|
||||
event.tool,
|
||||
event.input,
|
||||
"",
|
||||
@@ -262,6 +305,7 @@ fun foldEvent(items: List<TranscriptItem>, entry: SeqEvent): List<TranscriptItem
|
||||
TranscriptItem.ToolRun(
|
||||
entry.seq,
|
||||
event.id,
|
||||
runIdFor(items, event.id),
|
||||
"tool",
|
||||
"",
|
||||
event.output,
|
||||
@@ -908,7 +952,7 @@ fun SessionScreen(
|
||||
// reason: the working indicator appearing and disappearing is another insertion
|
||||
// at the same end. Paging older history is the opposite insertion and was
|
||||
// already fine, and stays fine, because a key survives both.
|
||||
items(rows.asReversed(), key = { it.seq }) { row ->
|
||||
items(rows.asReversed(), key = { it.key }) { row ->
|
||||
when (row) {
|
||||
is TranscriptRow.Tools ->
|
||||
ToolGroup(
|
||||
|
||||
@@ -32,26 +32,34 @@ import androidx.compose.ui.unit.dp
|
||||
*/
|
||||
sealed class TranscriptRow {
|
||||
/**
|
||||
* This row's identity in the list, taken from the first transcript event behind it.
|
||||
* This row's identity in the list, which must survive everything that can happen to the row.
|
||||
*
|
||||
* See [TranscriptItem.seq]: the list is keyed by this so that inserting a new message at one
|
||||
* end, or a page of history at the other, moves the rows and not the reader.
|
||||
* The list is keyed by this so that inserting a new message at one end, or a page of history at
|
||||
* the other, moves the rows and not the reader. That makes it the load-bearing value on this
|
||||
* screen: when a key changes, the list loses its anchor and the transcript steps under whoever
|
||||
* is reading it.
|
||||
*
|
||||
* A tool row therefore keys on [TranscriptItem.ToolRun.runId] rather than on a sequence number,
|
||||
* and it is the *same* value whether the run is drawn as one card or as a group. A lone call
|
||||
* that gains a neighbour becomes a group without changing identity, which is the case a
|
||||
* seq-based key got wrong: the row the reader was looking at was replaced rather than updated.
|
||||
* Everything else keys on the seq of the event behind it, which never moves.
|
||||
*/
|
||||
abstract val seq: Long
|
||||
abstract val key: Any
|
||||
|
||||
data class Single(val item: TranscriptItem) : TranscriptRow() {
|
||||
override val seq: Long
|
||||
get() = item.seq
|
||||
override val key: Any
|
||||
get() = (item as? TranscriptItem.ToolRun)?.runId ?: item.seq
|
||||
}
|
||||
|
||||
/** Two or more calls with nothing between them; drawn as one collapsed card. */
|
||||
data class Tools(val calls: List<TranscriptItem.ToolRun>) : TranscriptRow() {
|
||||
/** Stable across reloads because it is the first call's own id. */
|
||||
/** The run's own name, which every call in it already carries. */
|
||||
val id: String
|
||||
get() = calls.first().id
|
||||
get() = calls.first().runId
|
||||
|
||||
override val seq: Long
|
||||
get() = calls.first().seq
|
||||
override val key: Any
|
||||
get() = id
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,10 +83,16 @@ fun groupToolRuns(items: List<TranscriptItem>): List<TranscriptRow> {
|
||||
}
|
||||
|
||||
items.forEach { item ->
|
||||
if (item is TranscriptItem.ToolRun) run += item
|
||||
else {
|
||||
// Grouped by the run each call says it belongs to, not by adjacency worked out here.
|
||||
// Adjacency is the same answer most of the time and a worse one at the edges: a call
|
||||
// arriving next to an existing run, or a page of history arriving in front of one, both
|
||||
// change which call is *first*, and a group named after its first member is a different
|
||||
// group every time that happens.
|
||||
if (item is TranscriptItem.ToolRun && (run.isEmpty() || run.first().runId == item.runId)) {
|
||||
run += item
|
||||
} else {
|
||||
flush()
|
||||
rows += TranscriptRow.Single(item)
|
||||
if (item is TranscriptItem.ToolRun) run += item else rows += TranscriptRow.Single(item)
|
||||
}
|
||||
}
|
||||
flush()
|
||||
|
||||
Reference in new issue
Block a user