Skip to content

fix(chat): make the transcript follow the answer as one scroll model - #174

Merged
mrsibe merged 1 commit into
mainfrom
fix/chat-scroll-model
Sep 28, 2026
Merged

mrsibe merged 1 commit into
mainfrom
fix/chat-scroll-model

Conversation

@mrsibe

@mrsibe mrsibe commented Sep 28, 2026 •

Copy link
Copy Markdown
Owner

What does this PR do?

Reworks the chat transcript's auto-scroll into a single follow model:
follow only while the reader is near the bottom, stop the moment they scroll
up, reserve the composer's real height, and offer one icon-only back-to-bottom
control.

Why?

Three reported symptoms share one cause:

  • Streaming dragged the view back down even after the reader scrolled up to
    re-read a passage.
  • "Back to bottom" landed with the last line still behind the floating
    composer, because it scrolled to an anchor and the composer's height was a
    hardcoded pb-32 that a growing scope row or a multi-line question outpaced.
  • Height that arrived without a new message (Markdown reflow, code
    highlighting, images, an opened source disclosure) did not keep the follow
    alive.

What changed?

  • Follow is decided by scrollHeight - scrollTop - clientHeight against a 48px
    threshold, clamped at zero for sub-pixel layout, instead of
    scrollTop + clientHeight === scrollHeight.
  • A ResizeObserver on the transcript drives the follow from content height,
    so streaming and reflow both keep the end in view — only while pinned.
  • scrollToBottom targets the viewport's true bottom
    (scrollTo({ top: scrollHeight })), not an anchor's scrollIntoView.
  • ProcessPanel measures the composer and publishes --composer-reserve; the
    transcript's padding-bottom, the scroll fade height and the back-to-bottom
    control's offset all read that value.
  • The back-to-bottom control is a circular, icon-only ↓ centred above the
    composer (32×32, tooltip "Back to bottom" / "回到底部"). Smooth for a
    deliberate jump; instant while an answer streams or under
    prefers-reduced-motion. A programmatic lock keeps the animation from
    unfollowing part-way, and wheel/touch/keyboard input cancels it.
  • The transcript remounts per session, so a conversation always opens at its
    end.
  • The scroll arithmetic lives in chat/stickToBottom.ts with
    test/stickToBottom.test.ts; DESIGN.md describes the new model.

How was this tested?

  • npm run typecheck — passes
  • npm test — 428 pass (incl. 11 new stickToBottom cases: threshold
    boundary, fractional pixels, negative clamp)
  • npm run build — passes
  • npm run check:design — no violations
  • npm run lint — no findings in the changed files (existing any warnings
    unchanged)

Screenshots / recordings

Required for meaningful UI/UX changes. Not attached — no running desktop
session was available.
The reviewer should confirm in npm run dev:
the ↓ position above single-line / multi-line / scope-expanded composers, the
smooth jump, and that the last line stays clear of the input.

Before After

Checklist

  • I have reviewed my own changes.
  • npm run typecheck passes.
  • npm run build passes.
  • I have tested the affected user workflow.
  • I have not included unrelated changes.
  • I have updated documentation when necessary.

Desktop / build changes

  • Not applicable

The transcript's auto-scroll had three symptoms that share one cause: it
treated "scroll to bottom" as an anchor scroll and the composer's height as a
constant.

- Follow only while the reader is near the bottom (48px, measured as
  `scrollHeight - scrollTop - clientHeight`), so scrolling up to re-read stops
  the follow instead of fighting it. The distance is clamped at zero for
  sub-pixel layout.
- Drive the follow from the content's height with a `ResizeObserver`, not only
  from the messages array: streaming grows one message, and Markdown reflow, a
  highlighted code block, a decoded image or an opened source disclosure change
  the height with no message event.
- Land on the viewport's true bottom (`scrollTo({ top: scrollHeight })`) rather
  than an anchor's `scrollIntoView`, which stopped short of the reserved
  composer space and left the last line behind the input.
- Measure the floating composer with a `ResizeObserver` and publish it as
  `--composer-reserve`; the transcript's padding, the scroll fade and the
  back-to-bottom control all read that one value. The old fixed `pb-32` reserve
  could not track a growing scope row or a multi-line question.
- Turn the back-to-bottom control into a circular, icon-only `↓` centred above
  the composer. Smooth for a deliberate jump, instant while an answer streams
  or under `prefers-reduced-motion`; a programmatic lock keeps the animation
  from unfollowing halfway down.
- Remount the transcript per session so every conversation opens at its end.

The scroll arithmetic moves to `chat/stickToBottom.ts` and is covered by
`test/stickToBottom.test.ts`.
@github-actions github-actions Bot added the bug Something isn't working label Sep 28, 2026
@mrsibe
mrsibe merged commit 5c8be4e into main Sep 28, 2026
5 checks passed
@mrsibe
mrsibe deleted the fix/chat-scroll-model branch September 28, 2026 13:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant