From f402a1f6c80fe9200531e659c38e7064d4e544fa Mon Sep 17 00:00:00 2001 From: iris <2+iris@noreply.localhost> Date: Fri, 28 Aug 2026 23:32:35 -0400 Subject: [PATCH] Hold the bottom while typing, and put the status where the answer goes **Typing moved the newest message out of sight.** The list re-pinned on a new item and nothing else, but the two things that shrink it while somebody writes a reply are the field growing from one line to four and the keyboard opening under it -- neither of which is a new item. So the message being replied to drifted upward, and the view only came back when the reply was finally sent, which is the one moment it did not matter. It now watches the viewport as well as the item count, and only acts while already pinned. **The status has moved out of the corner.** As a label up there it said something about the session; at the end of the transcript it says something about a place -- this is where the next thing appears -- and that is where the reader is already looking, because it is where the last message is. `exited` is still said, in the same place. Removing the corner label without it would have left a session whose process is gone looking exactly like one waiting for input, on a screen whose whole purpose is typing at it. Verified on the phone-sized case rather than reasoned about: three lines of text with the keyboard open keeps the newest message directly above the field, and sending shows "working" immediately under the sent message. --- .../kotlin/com/example/aiapp/SessionScreen.kt | 42 +++++++++++++++++-- 1 file changed, 39 insertions(+), 3 deletions(-) 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 d656440..b8718c6 100644 --- a/app/androidApp/src/main/kotlin/com/example/aiapp/SessionScreen.kt +++ b/app/androidApp/src/main/kotlin/com/example/aiapp/SessionScreen.kt @@ -262,9 +262,14 @@ fun SessionScreen( // A new item at the newest end shifts every index by one, so the view // has to step back to 0 to stay put. One item, instantly -- not a // journey through the transcript. + // Anything that changes how much room the list has, as well as a new + // item arriving. Typing is the case that gets missed: the field grows + // from one line to four and the keyboard opens under it, and neither + // is a new message, so watching the item count alone leaves the newest + // text drifting out of sight while somebody writes a reply to it. LaunchedEffect(listState) { - snapshotFlow { items.size } - .collect { count -> if (followTail && count > 0) listState.scrollToItem(0) } + snapshotFlow { Pair(items.size, listState.layoutInfo.viewportSize.height) } + .collect { (count, _) -> if (followTail && count > 0) listState.scrollToItem(0) } } // Reaching the far end of what is loaded -- the oldest item, which in // this layout is the last index -- fetches the page before it. @@ -393,7 +398,6 @@ fun SessionScreen( color = MaterialTheme.colorScheme.onSurfaceVariant, ) } - StatusText(status) // Beside the provider it reports on, which is the line directly to its left. // // Its real home is this provider's settings, which do not exist yet; until they do, @@ -434,6 +438,38 @@ fun SessionScreen( contentPadding = PaddingValues(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp), ) { + // Where the next thing will appear: at the end of what has + // happened, which in this layout is the top of the list. + // In the corner it was a label about the session; here it + // is a place, and the eye is already there because that is + // where the newest message is. + if (running) { + item { + Row(verticalAlignment = Alignment.CenterVertically) { + CircularProgressIndicator( + modifier = Modifier.width(14.dp).height(14.dp), + strokeWidth = 2.dp, + ) + Spacer(Modifier.width(8.dp)) + Text( + if (status == "compacting") "compacting" else "working", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + } else if (status == "exited") { + // Still said somewhere: a session whose process is gone + // cannot be typed at, and with the corner label removed + // nothing else on this screen would mention it. + item { + Text( + "exited", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } // Reversed to match the layout, so index 0 is the newest and // the reader still sees them in the order they happened. items(items.asReversed()) { item ->