diff --git a/PLAN.md b/PLAN.md index d81d991..b4fbf1e 100644 --- a/PLAN.md +++ b/PLAN.md @@ -624,13 +624,6 @@ dev-updater (Kotlin 2.4.x, CMP 1.11.x, JDK 21). Screens: output; a spinner while `ToolStart` has no matching `ToolEnd`). - Question cards inline: option buttons for AskUserQuestion, allow/deny for permissions, free-text where allowed. - - Expanding a row **opens downwards**: whichever end the reader pressed — - a group's heading or the bar at its foot — is the end that stays put, - and the row grows away from it. The transcript is laid out from the - bottom, so a row's bottom edge is anchored for free and the top one - has to be arranged; `toggleAnchored` measures the move and scrolls it - back (2026-08-30, asked for after groups opened upwards and sent their - own heading off the top of the screen). - Input bar: text, attach (camera/gallery/file), send — **always enabled**; mid-run sends become steering messages. - Top bar: model chip (tap to change), stop button while running, token 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 fe09e52..5a1c4c0 100644 --- a/app/androidApp/src/main/kotlin/com/example/aiapp/SessionScreen.kt +++ b/app/androidApp/src/main/kotlin/com/example/aiapp/SessionScreen.kt @@ -5,7 +5,6 @@ import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.PickVisualMediaRequest import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.Image -import androidx.compose.foundation.gestures.scrollBy import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -62,11 +61,8 @@ import java.util.concurrent.atomic.AtomicReference import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import kotlinx.coroutines.withTimeoutOrNull private const val RECONNECT_DELAY_MS = 1500L @@ -83,14 +79,6 @@ private const val RECONNECT_DELAY_MS = 1500L */ private const val HISTORY_LOOKAHEAD = 8 -/** - * How long to wait for a row to finish changing size before giving up on holding its top edge. - * - * Generous, because it is only reached when the answer never comes: the row is measured on the very - * next layout in every ordinary case. - */ -private const val ANCHOR_TIMEOUT_MS = 500L - /** * What the transcript renders: the event stream folded into displayable rows (see [foldEvent]). The * stream is the only data source -- opening this screen replays from seq 0, and a reconnect resumes @@ -681,41 +669,6 @@ fun SessionScreen( } } - /** - * Changes a row's height while the end the reader pressed stays where it is. - * - * The transcript is laid out from the bottom, so every row's *bottom* edge is what the list - * holds still and all growth goes upward. That is right for the bar at the foot of an open - * group -- shut it from there and what follows it does not move, which is what the reader is - * looking at. It is exactly wrong for a heading: opening a group from the top used to send the - * heading up off the screen and fill the space above it, so the calls appeared on the far side - * of the control that produced them. - * - * So [RowEdge.Bottom] is the list's own behaviour and does nothing extra, and [RowEdge.Top] - * measures where the row's top edge was, lets the change land, and scrolls by however far it - * moved. It has to be measured rather than worked out: only the layout knows how tall an open - * group is, and it depends on the calls in it. - * - * Given up on after [ANCHOR_TIMEOUT_MS] rather than waited on forever -- a row that never - * settles is one that is no longer on screen, and the reader has moved on. - */ - fun toggleAnchored(key: Any, edge: RowEdge, toggle: () -> Unit) { - fun topOf() = - listState.layoutInfo.visibleItemsInfo - .firstOrNull { it.key == key } - ?.let { it.offset + it.size } - val before = topOf() - toggle() - if (edge == RowEdge.Bottom || before == null) return - scope.launch { - val after = - withTimeoutOrNull(ANCHOR_TIMEOUT_MS) { - snapshotFlow { topOf() }.filterNotNull().first { it != before } - } ?: return@launch - listState.scrollBy((after - before).toFloat()) - } - } - // A compaction reports nothing about its own progress -- measured against the CLI, which // says it has started, and then says nothing at all until it is done. So what this counts is // the one thing anybody here can measure: how long it has been going. A bar filling up would @@ -1172,23 +1125,16 @@ fun SessionScreen( ToolGroup( group = row, expanded = row.id in expandedGroups, - onToggle = { edge -> - toggleAnchored(row.key, edge) { - expandedGroups = - if (row.id in expandedGroups) expandedGroups - row.id - else expandedGroups + row.id - } + onToggle = { + expandedGroups = + if (row.id in expandedGroups) expandedGroups - row.id + else expandedGroups + row.id }, isToolExpanded = { it in expandedTools }, - // Anchored on the group, not the call: opening one call makes - // the whole group taller, and the heading the reader is under - // is the group's. onToolToggle = { id -> - toggleAnchored(row.key, RowEdge.Top) { - expandedTools = - if (id in expandedTools) expandedTools - id - else expandedTools + id - } + expandedTools = + if (id in expandedTools) expandedTools - id + else expandedTools + id }, onAnswer = { questionId, answers -> act { @@ -1212,12 +1158,10 @@ fun SessionScreen( tool = item, expanded = item.id in expandedTools, onToggle = { - toggleAnchored(row.key, RowEdge.Top) { - expandedTools = - if (item.id in expandedTools) - expandedTools - item.id - else expandedTools + item.id - } + expandedTools = + if (item.id in expandedTools) + expandedTools - item.id + else expandedTools + item.id }, onAnswer = { questionId, answers -> act { @@ -1259,12 +1203,10 @@ fun SessionScreen( item = item, expanded = item.seq in expandedNotes, onToggle = { - toggleAnchored(row.key, RowEdge.Top) { - expandedNotes = - if (item.seq in expandedNotes) - expandedNotes - item.seq - else expandedNotes + item.seq - } + expandedNotes = + if (item.seq in expandedNotes) + expandedNotes - item.seq + else expandedNotes + item.seq }, ) } 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 d7bae76..4d53dd0 100644 --- a/app/androidApp/src/main/kotlin/com/example/aiapp/ToolRows.kt +++ b/app/androidApp/src/main/kotlin/com/example/aiapp/ToolRows.kt @@ -99,20 +99,6 @@ fun groupToolRuns(items: List): List { return rows } -/** - * Which end of a row a reader acted on, and therefore which end must not move. - * - * A row has two controls at opposite ends -- the heading that opens it and the bar that shuts it - * again -- and the reader's finger is on one of them. Whichever it is has to stay where it is while - * the row changes size, or the thing they just pressed slides out from under them. The list anchors - * every row's bottom edge by default (see the transcript's `reverseLayout`), so [Bottom] is what - * happens on its own and [Top] is what has to be arranged. - */ -enum class RowEdge { - Top, - Bottom, -} - /** * Several calls under one heading, closed until somebody asks. * @@ -127,15 +113,14 @@ enum class RowEdge { fun ToolGroup( group: TranscriptRow.Tools, expanded: Boolean, - /** Told which end was pressed, because this row has a control at each -- see [RowEdge]. */ - onToggle: (RowEdge) -> Unit, + onToggle: () -> Unit, isToolExpanded: (String) -> Boolean, onToolToggle: (String) -> Unit, onAnswer: (questionId: String, answers: List) -> Unit, image: @Composable (String) -> Unit, ) { if (!expanded) { - Card(Modifier.fillMaxWidth().clickable { onToggle(RowEdge.Top) }) { + Card(Modifier.fillMaxWidth().clickable(onClick = onToggle)) { Text( "Called ${group.calls.size} tools", style = MaterialTheme.typography.titleSmall, @@ -148,7 +133,7 @@ fun ToolGroup( Text( "Called ${group.calls.size} tools", style = MaterialTheme.typography.titleSmall, - modifier = Modifier.fillMaxWidth().clickable { onToggle(RowEdge.Top) }.padding(12.dp), + modifier = Modifier.fillMaxWidth().clickable(onClick = onToggle).padding(12.dp), ) group.calls.forEach { call -> ToolCard( @@ -159,9 +144,7 @@ fun ToolGroup( image = image, ) } - // Shutting it from here anchors the other end: the reader is at the bottom of a long - // group, and what they are looking at is what follows it. - CollapseBar { onToggle(RowEdge.Bottom) } + CollapseBar(onToggle) } }