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:
irisandClaude Opus 5 committed 2026-08-30 03:03:55 -04:00
1 parent 7190eac6f3
commit ec40499ba4
3 files changed
+19 -101

No files matched your search

@@ -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
},
)
}