Merge remote-tracking branch 'origin/main'

This commit is contained in:
iris committed 2026-08-29 23:43:26 -04:00
commit 694535badc
3 files changed
+182 -3

No files matched your search

@@ -177,6 +177,52 @@ sealed class TranscriptItem {
) : TranscriptItem()
}
/**
* Puts a page of older items in front of the ones already loaded, healing any tool call the page
* boundary cut in two.
*
* A boundary lands wherever it lands, and roughly half the time that is between a call and its
* result. The newer page then holds a `ToolEnd` whose start it never saw, which [foldEvent] draws
* as a row of its own -- correctly, because a call that renders as nothing is indistinguishable
* from one that never happened. When the older page arrives it brings the real `ToolStart`, and
* concatenating the two lists left *both*: the same call twice, once as a proper card and once as a
* nameless placeholder. Visible as a run of four calls reporting "Called 5 tools", and worse than
* the miscount -- the extra row is at the join, so it also moves everything the reader was looking
* at.
*
* Merged by the call's own id rather than by position, because position is exactly what a page
* boundary destroys. The older row wins on what a start knows (the tool's name, its input) and the
* newer on what an end knows (the output, and whether it finished), which is the only way round
* that loses nothing.
*/
fun joinPages(earlier: List<TranscriptItem>, later: List<TranscriptItem>): List<TranscriptItem> {
val startedEarlier =
earlier.filterIsInstance<TranscriptItem.ToolRun>().mapTo(mutableSetOf()) { it.id }
if (startedEarlier.isEmpty()) return earlier + later
val endedLater =
later
.filterIsInstance<TranscriptItem.ToolRun>()
.associateBy { it.id }
.filterKeys { it in startedEarlier }
if (endedLater.isEmpty()) return earlier + later
val healed = earlier.map { row ->
val half = (row as? TranscriptItem.ToolRun)?.let { endedLater[it.id] }
if (row is TranscriptItem.ToolRun && half != null) {
row.copy(
output = half.output,
done = half.done,
// Kept from both halves: a question or an image can be attached to either,
// depending on which side of the boundary its event fell.
asks = row.asks + half.asks,
images = row.images + half.images,
)
} else {
row
}
}
return healed + later.filterNot { it is TranscriptItem.ToolRun && it.id in endedLater }
}
fun foldEvent(items: List<TranscriptItem>, entry: SeqEvent): List<TranscriptItem> =
when (val event = entry.event) {
is SessionEvent.UserMessage -> items + TranscriptItem.UserMsg(entry.seq, event.text)
@@ -636,7 +682,7 @@ fun SessionScreen(
earlier = foldEvent(earlier, entry)
}
}
items = earlier + items
items = joinPages(earlier, items)
}
} catch (_: ApiException) {
// Leave `moreHistory` alone: the next scroll asks again.