From c9e9ddb62fbdd99923b26a35041a3aa013790c2e Mon Sep 17 00:00:00 2001 From: iris <2+iris@noreply.localhost> Date: Mon, 31 Aug 2026 13:46:14 -0400 Subject: [PATCH] Split a stood-down run across spacers Constraints can hold `Can't represent a width of 0 and height of 273238 in Constraints`, from `Modifier.height` during measure, which took the app down on opening one particular session. Compose packs a Constraints into a single Long, and at a width of zero that leaves eighteen bits for the height -- so 262,143px is the ceiling and a taller spacer throws. Collapsing every stood-down row into one spacer is what made that reachable. It is the change that stopped the per-frame cost growing with the conversation, and it also turned "the history above the window" into a single fixed height -- which for a long conversation read at the newest end is the whole transcript. The one that crashed was 273,238px. Split rather than clamped: the height is load-bearing. It is what keeps the transcript's total the same as the rows it stands in for, so shortening it would move everything under the reader -- a silent wrong answer in place of a loud one. The gaps the arrangement inserts between the pieces come out of the total for the same reason. The cap is 100,000px rather than the 262,143 that would just fit, because the limit depends on how many bits the width took: a spacer sized against today's screen width is a crash waiting for a wider one. Verified by forcing the split -- built with the cap at 5,000px, a run came out as ten spacers with the transcript intact and nothing reporting an unbuilt row -- then restored. Testing it only at the real cap would have tested the branch that cannot fail on any conversation reachable here. Co-Authored-By: Claude Opus 5 --- .../com/example/aiapp/TranscriptScroll.kt | 51 +++++++++++++++++-- 1 file changed, 47 insertions(+), 4 deletions(-) diff --git a/app/androidApp/src/main/kotlin/com/example/aiapp/TranscriptScroll.kt b/app/androidApp/src/main/kotlin/com/example/aiapp/TranscriptScroll.kt index 069221e..fcc4da2 100644 --- a/app/androidApp/src/main/kotlin/com/example/aiapp/TranscriptScroll.kt +++ b/app/androidApp/src/main/kotlin/com/example/aiapp/TranscriptScroll.kt @@ -418,6 +418,37 @@ class TranscriptScroll(internal val scroll: ScrollState) { * Getting that wrong does not look like a spacing bug; it shortens the content, which moves * everything the reader is looking at. */ + /** + * The spacers that stand in for rows [run], because one of them will not always do. + * + * `Modifier.height` turns into a fixed `Constraints`, and Compose packs a Constraints into a + * single Long -- with a width of zero it has eighteen bits for the height, so anything over + * 262,143px throws `Can't represent a width of 0 and height of N in Constraints` and takes the + * app down during measure. Collapsing every stood-down row into one spacer is what made that + * reachable: a long conversation scrolled to the newest end has its whole history above the + * window, and one seen here was 273,238px of it. + * + * Split rather than clamped, because the height is load-bearing -- it is what holds the + * transcript's total the same as the rows it replaces, and shortening it would move everything + * under the reader. The gaps the arrangement inserts between the pieces come out of the total + * for the same reason. + */ + fun spacerHeights(run: IntRange): List { + val total = runHeight(run) + if (total <= 0) return emptyList() + if (total <= SPACER_MAX) return listOf(total) + var pieces = 2 + while (true) { + val body = total - (pieces - 1) * spacing + if (body <= 0) return listOf(total.coerceAtMost(SPACER_MAX)) + if ((body + pieces - 1) / pieces <= SPACER_MAX) { + val each = body / pieces + return List(pieces) { each + if (it == 0) body - each * pieces else 0 } + } + pieces++ + } + } + fun runHeight(run: IntRange): Int { if (run.isEmpty() || order.isEmpty()) return 0 var total = 0 @@ -854,10 +885,13 @@ val TRANSCRIPT_PADDING: PaddingValues = PaddingValues(16.dp) @Composable private fun StoodDownRun(state: TranscriptScroll, run: IntRange) { if (run.isEmpty()) return - DebugStats.count("rows stood down as one spacer") - Spacer( - Modifier.fillMaxWidth().height(with(LocalDensity.current) { state.runHeight(run).toDp() }) - ) + val heights = state.spacerHeights(run) + DebugStats.count("rows stood down as spacers") + DebugStats.atLeast("spacers standing in for a run", heights.size.toLong()) + val density = LocalDensity.current + heights.forEachIndexed { index, height -> + key(index) { Spacer(Modifier.fillMaxWidth().height(with(density) { height.toDp() })) } + } } @Composable @@ -919,5 +953,14 @@ private const val STAND_UP_PER_FRAME = 2 */ private const val SEED_SCREENS = 2 +/** + * The tallest a single spacer may be; see [TranscriptScroll.spacerHeights]. + * + * Comfortably under the 262,143px that a fixed-width `Constraints` can hold, rather than at it: the + * limit depends on how many bits the width took, and a spacer that is nearly the maximum is a crash + * waiting for a wider screen. + */ +private const val SPACER_MAX = 100_000 + /** What a row is assumed to be worth before any of them have been measured. */ private const val ROW_GUESS = 800