Notice a row arriving, and stop subcomposing the list at the keyboard

Two faults from making the retained window lazy, both of them mine.

A screen of blank between the last message and the box it was typed in.
The window was recomputed when the view had moved far enough, and a
message arriving does not move the view -- so the new row fell outside
the window and stood in as a spacer of its guessed height. The version
number the check compares against is only bumped by the recompute it
guards, so asking before refreshing meant never noticing. It refreshes
first now, and the window also watches how many rows there are, because
a row arriving is the case it exists to catch and the one that does not
announce itself through the scroll position.

And the keyboard, which was the most expensive thing on the screen. The
visible height came from a `BoxWithConstraints` wrapped around the
transcript -- that is a `SubcomposeLayout`, and the IME animation changes
the visible height on every frame of its slide, so the entire transcript
was being subcomposed again for each of them. The same number read in
the layout phase, from the scroll container's own measurement, makes it a
relayout instead, and the rows keep the measurements they already have.

Checked on the emulator: the newest message sits against the composer
with the keyboard up, and there is no gap.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
irisandClaude Opus 5 committed 2026-08-31 03:41:10 -04:00
1 parent 2b24362cc4
commit 9052e5f55e
2 files changed
+29 -9

No files matched your search

@@ -8,7 +8,6 @@ import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@@ -1415,13 +1414,10 @@ fun SessionScreen(settings: ServerSettings, summary: SessionSummary, onBack: ()
// is no frame in which the transcript is somewhere other than where it was left.
val settled = !restoring && !listState.settling
Box(Modifier.weight(1f).fillMaxWidth()) {
BoxWithConstraints(Modifier.fillMaxSize()) {
Box(Modifier.fillMaxSize()) {
TranscriptColumn(
rows = rows,
state = listState,
// A scrollable child is measured with no height bound, so the content cannot
// ask how tall the visible area is; this is the only place that knows.
viewportHeight = maxHeight,
contentPadding = TRANSCRIPT_PADDING,
spacing = TRANSCRIPT_SPACING,
modifier =