Skip to content

Fix the chat not scrolling to the end of an answer#537

Merged
tobocop2 merged 3 commits into
mainfrom
fix/chat-final-scroll-after-layout
Jul 16, 2026
Merged

Fix the chat not scrolling to the end of an answer#537
tobocop2 merged 3 commits into
mainfrom
fix/chat-final-scroll-after-layout

Conversation

@tobocop2

@tobocop2 tobocop2 commented Jul 16, 2026

Copy link
Copy Markdown
Owner

Problem

When an answer finished, the chat stopped short of the end of it. The last part, usually the sources, sat below the visible area and you had to scroll down by hand to read it.

Solution

Textual can keep a view pinned to the bottom as content arrives, and let go once the reader scrolls away. The chat now uses that, replacing the hand-written scrolling it had, which measured the answer at the wrong moment and left the end of it off screen.

This is the second bug in that scrolling code, after #294, so it is replaced rather than patched again.

A finished answer could be left with its tail, usually the citations,
below the fold, and nothing would bring it back: the view stayed put with
the question still on screen above an answer whose end you had to scroll
to by hand.

The chat log followed the answer by hand: a tick every 150ms measured the
log and scrolled it to the bottom, with a pair of flags deciding whether
the user had scrolled away and wanted to be left alone. Measuring is the
problem. While the model streams, the answer often still fits, so there
is nothing to scroll; it only outgrows the pane when the answer finishes
and renders the tail it had buffered plus the citations. Markdown mounts
those asynchronously, so the last scroll of the turn ran against a widget
that had not grown yet, did nothing, and left no tick behind to fix it.

Textual has this: an anchored widget stays scrolled to the bottom as
content arrives, until the user scrolls away. It re-pins the scroll while
it arranges the layout, off the size it just computed, so there is no
moment where the view is behind the content and no measurement of ours to
get wrong. It also handles the scrollbar and the wheel, which our version
never did.

So the follower goes, and a turn anchors the log instead. This is the
second bug in that hand-written follower, after the auto-follow disabling
itself in #294.

Tests: the scroll ones drove _scroll_to_bottom directly, so they are now
written against what the reader sees, and all of them set up by sending a
question rather than anchoring the log themselves, which is what lets
them fail when the anchoring goes missing.
@tobocop2
tobocop2 force-pushed the fix/chat-final-scroll-after-layout branch from 2814cc2 to 48d4c10 Compare July 16, 2026 03:17
@tobocop2 tobocop2 changed the title Scroll to the end of a finished answer Let Textual keep the chat log scrolled to the answer Jul 16, 2026
@tobocop2 tobocop2 changed the title Let Textual keep the chat log scrolled to the answer Fix chat answers ending below the fold: let Textual anchor the log Jul 16, 2026
@tobocop2 tobocop2 changed the title Fix chat answers ending below the fold: let Textual anchor the log Fix the chat not scrolling to the end of an answer Jul 16, 2026
tobocop2 added 2 commits July 16, 2026 01:37
The scroll tests read scroll_y and max_scroll_y back after a single
pilot.pause(), which settles none of what they depend on. The scroll
bindings animate, so scroll_y samples somewhere on the easing curve at
whatever point wall-clock allowed: on a loaded runner it had not moved
at all yet and "the reader scrolled up" asserted 135 < 135. A bubble
also only buffers appended content until compose hands it a content
widget, so an answer grown before that lands leaves max_scroll_y at 0
and the preconditions compare 0 to 0.

Wait on Textual's own wait_for_scheduled_animations, which drains
pending messages, runs the animations out, and drains again. The turn
helper now also asserts the bubble composed, so a test that would
otherwise pass against an empty bubble fails instead.
AGENTS.md asks TUI tests to go through pilot.press() rather than call
action_* methods, and this is a good demonstration of why. Pressing
PgUp does not scroll the log at all: the chat input holds focus and
takes the key, since the binding is not a priority one. Calling
action_scroll_up() jumped over the binding and hid that.

Escape first, then PgUp, which is what the reader does and what the
modal design intends: you type in insert mode, and escape to read.
Focus moves to the log there, so PgUp and G land on it.
@tobocop2
tobocop2 merged commit c62a0f2 into main Jul 16, 2026
30 of 34 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant