diff --git a/app/androidApp/src/main/kotlin/com/example/aiapp/Markdown.kt b/app/androidApp/src/main/kotlin/com/example/aiapp/Markdown.kt index f9ae47b..1e226c5 100644 --- a/app/androidApp/src/main/kotlin/com/example/aiapp/Markdown.kt +++ b/app/androidApp/src/main/kotlin/com/example/aiapp/Markdown.kt @@ -3,6 +3,7 @@ package com.example.aiapp import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.Stable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier @@ -166,6 +167,7 @@ private fun parsedMarkdown(text: String, replies: ParsedReplies): State { * itself on the way to being finished. It is dropped with the screen, and emptied by the stream * reset that drops the rows it describes. */ +@Stable class ParsedReplies { private val parsed = ConcurrentHashMap() diff --git a/app/androidApp/src/main/kotlin/com/example/aiapp/MemoryNote.kt b/app/androidApp/src/main/kotlin/com/example/aiapp/MemoryNote.kt index d5a7808..995bdb8 100644 --- a/app/androidApp/src/main/kotlin/com/example/aiapp/MemoryNote.kt +++ b/app/androidApp/src/main/kotlin/com/example/aiapp/MemoryNote.kt @@ -27,6 +27,7 @@ import androidx.compose.ui.unit.dp */ @Composable fun AssistantMessage(text: String, replies: ParsedReplies, modifier: Modifier = Modifier) { + DebugStats.count("message composed") val parts = remember(text) { partsOf(text) } val only = parts.singleOrNull() if (only is MessagePart.Prose) { diff --git a/app/androidApp/src/main/kotlin/com/example/aiapp/SessionScreen.kt b/app/androidApp/src/main/kotlin/com/example/aiapp/SessionScreen.kt index ab78a69..27fce6a 100644 --- a/app/androidApp/src/main/kotlin/com/example/aiapp/SessionScreen.kt +++ b/app/androidApp/src/main/kotlin/com/example/aiapp/SessionScreen.kt @@ -35,6 +35,7 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.Immutable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue @@ -179,6 +180,7 @@ private fun Modifier.holdTopEdge(key: Any, held: TopEdgeHold, hold: (Int) -> Uni * stream is the only data source -- opening this screen replays from seq 0, and a reconnect resumes * from the last seq seen, so there is no separate history fetch to drift from it. */ +@Immutable sealed class TranscriptItem { /** * The transcript sequence number this row started at, and its identity on screen. diff --git a/app/androidApp/src/main/kotlin/com/example/aiapp/ToolRows.kt b/app/androidApp/src/main/kotlin/com/example/aiapp/ToolRows.kt index af8833f..69fb472 100644 --- a/app/androidApp/src/main/kotlin/com/example/aiapp/ToolRows.kt +++ b/app/androidApp/src/main/kotlin/com/example/aiapp/ToolRows.kt @@ -18,6 +18,7 @@ import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -38,6 +39,19 @@ import androidx.compose.ui.unit.dp * the transcript's own order is what paging and the event stream depend on, and one screen's idea * of "these belong together" must not reach back into it. */ +/** + * Immutable, and said so, because Compose cannot tell. + * + * A row is a value: it is rebuilt from the transcript rather than edited, and two rows describing + * the same events are equal. Compose infers stability from a class's fields, and a `List` field -- + * which several of these carry -- makes it assume the worst, so every composable taking one + * recomposed whenever anything above it did. A page of history landing recomposed all 148 loaded + * rows including the markdown inside them, measured as 701 compositions for 148 rows in one scroll, + * and that is what a page landing costs on top of the fetch itself. + * + * The promise this makes is real and has to stay true: nothing here is mutated after it is built. + */ +@Immutable sealed class TranscriptRow { /** * This row's identity in the list, which must survive everything that can happen to the row.