Revert "Open a row downwards, from whichever end was pressed"
This reverts commit f4d4c82. The anchoring it added made the transcript
jump on every expand and collapse, and left the list snapping back to the
bottom when somebody scrolled up, which is worse than the upward-opening
it was meant to fix.
Two things to look at when this is retried. `LazyListItemInfo.offset` in a
`reverseLayout` list is not obviously the coordinate space this assumed,
so `offset + size` may have been measuring the bottom edge -- the one the
list already holds -- rather than the top. And the anchoring scroll ran in
a coroutine that could still be pending when the reader started dragging;
`scrollBy` takes the default mutation priority, so it cancels that drag.
Verify the next attempt with `uiautomator dump` -- node bounds in device
pixels, before and after a toggle -- rather than by eye from screenshots.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7190eac6f3
commit
ec40499ba4
3 files changed
+19
-101
No files matched your search
@@ -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`).
|
output; a spinner while `ToolStart` has no matching `ToolEnd`).
|
||||||
- Question cards inline: option buttons for AskUserQuestion, allow/deny for
|
- Question cards inline: option buttons for AskUserQuestion, allow/deny for
|
||||||
permissions, free-text where allowed.
|
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**;
|
- Input bar: text, attach (camera/gallery/file), send — **always enabled**;
|
||||||
mid-run sends become steering messages.
|
mid-run sends become steering messages.
|
||||||
- Top bar: model chip (tap to change), stop button while running, token
|
- Top bar: model chip (tap to change), stop button while running, token
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import androidx.activity.compose.rememberLauncherForActivityResult
|
|||||||
import androidx.activity.result.PickVisualMediaRequest
|
import androidx.activity.result.PickVisualMediaRequest
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.gestures.scrollBy
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -62,11 +61,8 @@ import java.util.concurrent.atomic.AtomicReference
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.awaitCancellation
|
import kotlinx.coroutines.awaitCancellation
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.filterNotNull
|
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.coroutines.withTimeoutOrNull
|
|
||||||
|
|
||||||
private const val RECONNECT_DELAY_MS = 1500L
|
private const val RECONNECT_DELAY_MS = 1500L
|
||||||
|
|
||||||
@@ -83,14 +79,6 @@ private const val RECONNECT_DELAY_MS = 1500L
|
|||||||
*/
|
*/
|
||||||
private const val HISTORY_LOOKAHEAD = 8
|
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
|
* 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
|
* 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
|
// 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
|
// 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
|
// 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(
|
ToolGroup(
|
||||||
group = row,
|
group = row,
|
||||||
expanded = row.id in expandedGroups,
|
expanded = row.id in expandedGroups,
|
||||||
onToggle = { edge ->
|
onToggle = {
|
||||||
toggleAnchored(row.key, edge) {
|
expandedGroups =
|
||||||
expandedGroups =
|
if (row.id in expandedGroups) expandedGroups - row.id
|
||||||
if (row.id in expandedGroups) expandedGroups - row.id
|
else expandedGroups + row.id
|
||||||
else expandedGroups + row.id
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
isToolExpanded = { it in expandedTools },
|
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 ->
|
onToolToggle = { id ->
|
||||||
toggleAnchored(row.key, RowEdge.Top) {
|
expandedTools =
|
||||||
expandedTools =
|
if (id in expandedTools) expandedTools - id
|
||||||
if (id in expandedTools) expandedTools - id
|
else expandedTools + id
|
||||||
else expandedTools + id
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onAnswer = { questionId, answers ->
|
onAnswer = { questionId, answers ->
|
||||||
act {
|
act {
|
||||||
@@ -1212,12 +1158,10 @@ fun SessionScreen(
|
|||||||
tool = item,
|
tool = item,
|
||||||
expanded = item.id in expandedTools,
|
expanded = item.id in expandedTools,
|
||||||
onToggle = {
|
onToggle = {
|
||||||
toggleAnchored(row.key, RowEdge.Top) {
|
expandedTools =
|
||||||
expandedTools =
|
if (item.id in expandedTools)
|
||||||
if (item.id in expandedTools)
|
expandedTools - item.id
|
||||||
expandedTools - item.id
|
else expandedTools + item.id
|
||||||
else expandedTools + item.id
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onAnswer = { questionId, answers ->
|
onAnswer = { questionId, answers ->
|
||||||
act {
|
act {
|
||||||
@@ -1259,12 +1203,10 @@ fun SessionScreen(
|
|||||||
item = item,
|
item = item,
|
||||||
expanded = item.seq in expandedNotes,
|
expanded = item.seq in expandedNotes,
|
||||||
onToggle = {
|
onToggle = {
|
||||||
toggleAnchored(row.key, RowEdge.Top) {
|
expandedNotes =
|
||||||
expandedNotes =
|
if (item.seq in expandedNotes)
|
||||||
if (item.seq in expandedNotes)
|
expandedNotes - item.seq
|
||||||
expandedNotes - item.seq
|
else expandedNotes + item.seq
|
||||||
else expandedNotes + item.seq
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,20 +99,6 @@ fun groupToolRuns(items: List<TranscriptItem>): List<TranscriptRow> {
|
|||||||
return rows
|
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.
|
* Several calls under one heading, closed until somebody asks.
|
||||||
*
|
*
|
||||||
@@ -127,15 +113,14 @@ enum class RowEdge {
|
|||||||
fun ToolGroup(
|
fun ToolGroup(
|
||||||
group: TranscriptRow.Tools,
|
group: TranscriptRow.Tools,
|
||||||
expanded: Boolean,
|
expanded: Boolean,
|
||||||
/** Told which end was pressed, because this row has a control at each -- see [RowEdge]. */
|
onToggle: () -> Unit,
|
||||||
onToggle: (RowEdge) -> Unit,
|
|
||||||
isToolExpanded: (String) -> Boolean,
|
isToolExpanded: (String) -> Boolean,
|
||||||
onToolToggle: (String) -> Unit,
|
onToolToggle: (String) -> Unit,
|
||||||
onAnswer: (questionId: String, answers: List<String>) -> Unit,
|
onAnswer: (questionId: String, answers: List<String>) -> Unit,
|
||||||
image: @Composable (String) -> Unit,
|
image: @Composable (String) -> Unit,
|
||||||
) {
|
) {
|
||||||
if (!expanded) {
|
if (!expanded) {
|
||||||
Card(Modifier.fillMaxWidth().clickable { onToggle(RowEdge.Top) }) {
|
Card(Modifier.fillMaxWidth().clickable(onClick = onToggle)) {
|
||||||
Text(
|
Text(
|
||||||
"Called ${group.calls.size} tools",
|
"Called ${group.calls.size} tools",
|
||||||
style = MaterialTheme.typography.titleSmall,
|
style = MaterialTheme.typography.titleSmall,
|
||||||
@@ -148,7 +133,7 @@ fun ToolGroup(
|
|||||||
Text(
|
Text(
|
||||||
"Called ${group.calls.size} tools",
|
"Called ${group.calls.size} tools",
|
||||||
style = MaterialTheme.typography.titleSmall,
|
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 ->
|
group.calls.forEach { call ->
|
||||||
ToolCard(
|
ToolCard(
|
||||||
@@ -159,9 +144,7 @@ fun ToolGroup(
|
|||||||
image = image,
|
image = image,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// Shutting it from here anchors the other end: the reader is at the bottom of a long
|
CollapseBar(onToggle)
|
||||||
// group, and what they are looking at is what follows it.
|
|
||||||
CollapseBar { onToggle(RowEdge.Bottom) }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user