Draw an opened peer message as list units, and give a peer note its own key
A peer message opened was still one item of the transcript list, so every
block of it was composed, measured, placed and kept *alive* while any part
of it was on screen -- and the framework's own per-frame cost grows with how
many nodes are alive rather than how many are drawn. Measured on the
emulator, scrolling the same stretch with a 43KB message shut, opened, and
opened after this change:
shut opened opened, split
draw phase per frame 0.81ms 3.85ms 1.22ms
of that, the framework 0.39ms 3.15ms 0.42ms
frame total, median 16.9ms 24.3ms 21.1ms
So an opened message now costs about what a shut one does. That is the
shape the report from the phone had -- 11.80ms of draw phase with 79% of it
outside anything this app times -- which no counter here could attribute,
because "2 units visible" says one of them is enormous without saying which.
It says which now: the transcript section of the report names every visible
unit and its height, which is what found this.
The card is cut up rather than given up. A filled Material card is elevation
zero (`FilledCardTokens.ContainerElevation` is `Level0`), so there is no
shadow for a seam to show through: each piece paints the same fill, rounds
only the corners at the ends of the message, and keeps the 12dp inset the
card's own column had. Opening and shutting still hold the edge the reader
pressed, and now without a correction -- the list is keyed, so it holds the
item it is anchored on wherever the new ones land.
The crash this turned up is the more serious half. `placePeerNote` gives a
note the seq of the turn it started so it sorts above the reply it caused,
and argued the seq was free because it belongs to a status change and a
status draws no row. True, and about the wrong collision: two messages that
arrive during one turn are stamped with the same turn, so they became two
rows with one key and `LazyColumn` threw -- the app dying in the middle of
somebody reading. Two agents writing to a session mid-turn is an ordinary
afternoon. A note now keeps its own arrival seq as its identity while `seq`
stays the position it sorts at, and which value a row is keyed by moved onto
`TranscriptItem` itself, which also removes the `as? ToolRun` branch that
was doing the same job in `TranscriptRow.Single`.
`transcriptUnits` now says which two units collided if it ever happens
again. All the framework's message carries is the key, and when that key is
a seq it names neither row; two lines here answered in one run what had
taken an afternoon.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
80aaf286c2
commit
917eb9a7b3
6 files changed
+321
-51
No files matched your search
@@ -343,7 +343,7 @@ fun SessionScreen(settings: ServerSettings, summary: SessionSummary, onBack: ()
|
||||
// What is actually drawn: the transcript with runs of adjacent tool
|
||||
// calls folded into one row each, flattened into the list's units.
|
||||
val rows = remember(items) { groupToolRuns(items) }
|
||||
val units = remember(rows) { transcriptUnits(rows, replies) }
|
||||
val units = remember(rows, expandedNotes) { transcriptUnits(rows, replies, expandedNotes) }
|
||||
// The same list, readable from effects launched before this composition: an effect's closure
|
||||
// keeps the values of the composition that launched it, and both the anchor saver and the
|
||||
// restore need the units as they are *now*.
|
||||
@@ -967,6 +967,22 @@ fun SessionScreen(settings: ServerSettings, summary: SessionSummary, onBack: ()
|
||||
openMemories = if (text in openMemories) openMemories - text else openMemories + text
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens or closes one peer message, from whichever of its pieces was pressed.
|
||||
*
|
||||
* By seq rather than by unit, because an open message is several units and all of them shut it
|
||||
* -- it was one card before it was several items, and which piece the finger landed on is not
|
||||
* something the reader chose.
|
||||
*
|
||||
* No [toggleAnchored] here, and that is the difference between growing a row and adding items:
|
||||
* the list is keyed, so it holds the item it is anchored on wherever the new ones land. What
|
||||
* the reader tapped keeps its place because the list keeps it, not because a measurement
|
||||
* corrected it afterwards.
|
||||
*/
|
||||
fun togglePeer(seq: Long) {
|
||||
expandedNotes = if (seq in expandedNotes) expandedNotes - seq else expandedNotes + seq
|
||||
}
|
||||
|
||||
fun act(onDone: () -> Unit = {}, action: () -> Unit) {
|
||||
scope.launch {
|
||||
try {
|
||||
@@ -1147,6 +1163,11 @@ fun SessionScreen(settings: ServerSettings, summary: SessionSummary, onBack: ()
|
||||
" ${listState.layoutInfo.viewportSize.height}px," +
|
||||
" ${listState.layoutInfo.visibleItemsInfo.size}" +
|
||||
" units visible",
|
||||
visibleUnits(
|
||||
units,
|
||||
listState.layoutInfo.visibleItemsInfo,
|
||||
UNITS_START,
|
||||
),
|
||||
" ${expandedTools.size} tool calls and" +
|
||||
" ${expandedGroups.size} groups open",
|
||||
),
|
||||
@@ -1283,6 +1304,19 @@ fun SessionScreen(settings: ServerSettings, summary: SessionSummary, onBack: ()
|
||||
) { unit ->
|
||||
when (unit) {
|
||||
is TranscriptUnit.Block -> MarkdownText(unit.text, replies)
|
||||
is TranscriptUnit.PeerHead ->
|
||||
PeerHeadRow(
|
||||
unit.item,
|
||||
unit.open,
|
||||
onToggle = { togglePeer(unit.item.seq) },
|
||||
)
|
||||
is TranscriptUnit.PeerBlock ->
|
||||
PeerBlockRow(
|
||||
unit.text,
|
||||
replies,
|
||||
unit.last,
|
||||
onToggle = { togglePeer(unit.seq) },
|
||||
)
|
||||
is TranscriptUnit.Memory ->
|
||||
MemoryNote(
|
||||
unit.part,
|
||||
@@ -1481,20 +1515,13 @@ fun SessionScreen(settings: ServerSettings, summary: SessionSummary, onBack: ()
|
||||
is TranscriptItem.ClearedNote -> ClearedRow()
|
||||
is TranscriptItem.CompactedNote ->
|
||||
CompactedRow(item)
|
||||
// Never reached: a peer message is flattened
|
||||
// into its own units, so it is not a whole row.
|
||||
// Here because a `when` over the item kinds has to
|
||||
// stay exhaustive, and drawing nothing is how a
|
||||
// row that stopped being handled would look.
|
||||
is TranscriptItem.PeerNote ->
|
||||
PeerMessageRow(
|
||||
item = item,
|
||||
expanded = item.seq in expandedNotes,
|
||||
replies = replies,
|
||||
onToggle = {
|
||||
toggleAnchored(row) {
|
||||
expandedNotes =
|
||||
if (item.seq in expandedNotes)
|
||||
expandedNotes - item.seq
|
||||
else expandedNotes + item.seq
|
||||
}
|
||||
},
|
||||
)
|
||||
PeerHeadRow(item, open = false, onToggle = {})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user