Skip to content

feat(chat): let the message scroller run under the composer - #226

Closed
RoxyAsahi wants to merge 4 commits into
lioensky:mainfrom
RoxyAsahi:pr/zcode-composer-overlay
Closed

RoxyAsahi wants to merge 4 commits into
lioensky:mainfrom
RoxyAsahi:pr/zcode-composer-overlay

Conversation

@RoxyAsahi

Copy link
Copy Markdown
Contributor

Depends on #225. This branch is stacked on it, so the first three commits here are #225's. Only the last commit, feat(chat): let the message scroller run under the composer, is new. I'll rebase once #225 lands.

Summary

The message list used to stop at the top of the input area, so the scrollbar ended there too. Now the scroller runs to the bottom of the window and the input area floats over its last stretch.

  • chat-composer-inset.js measures the input area with a ResizeObserver. It publishes the composer height and the scrollbar width as CSS variables on <main> and adds .vcp-chat-composer-overlay.

  • chat-composer-inset.css does three things:

    • lays the input area over the scroller;
    • pads the scroller by the composer height plus a 20px fade;
    • masks the messages out behind the composer, with a short fade above it.

    The scrollbar column stays unmasked, so it reaches the bottom of the window.

  • Without the script the layout is unchanged.

  • When the composer grows (multi-line input), a view that follows the bottom stays pinned. A reader who has scrolled up stays where they are.

  • The turn navigator from feat(chat): turn navigator, back-to-bottom button and scroller under the composer #225 treats the padded stretch as outside the viewport, so the active bar matches what is visible above the composer.

Test plan

🤖 Generated with Claude Code

RoxyAsahi and others added 4 commits October 2, 2026 19:48
Clicking a message-content search result re-opens the topic, which turns
bottom-follow back on, then scrolls the hit into view 100ms later. In a long
topic the content-visibility messages the smooth scroll passes grow, the
follow ResizeObserver sees a taller list and pulls the view back to the
bottom, so the hit is never shown. Follow currently only turns off on user
input (wheel up, touch, scrollbar drag), and a programmatic jump has no way
to say "I am leaving the bottom on purpose".

ui-helpers.js gains releaseChatScrollFollow(): it bumps the scroll
generation, cancels any queued bottom scroll and turns follow off. Reaching
the bottom again re-enables follow exactly as before. searchManager calls it
right before scrollIntoView.

Follow flips are now funnelled through one setter that dispatches
`vcp-chat-follow-change` (exported as CHAT_FOLLOW_CHANGE_EVENT) on the
scroll container, so UI that depends on follow state can subscribe instead
of re-deriving it from geometry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A thin rail on the left of the main chat with one short bar per user
question. Hovering a bar magnifies it and its neighbours and shows a
card with the question and the start of the answer; clicking jumps to
that turn. The rail hides when the chat column is narrower than 864px
instead of squeezing in next to the messages; it also hides when the
topic has fewer than two questions. Idle bars are 8px; hovered ones
magnify to about 31 / 20 / 15px.

Settings > 界面与外观 gains 显示提问导航条 (enableTurnNavigator, on by
default). Turning it off disposes the navigator with all its observers;
renderer.js mounts or disposes it on global-settings-updated, which
fires after settings load and save.

Plain DOM plus styles/ui-system/turn-navigator.css. Entries come
straight from the message DOM: each .message-item.user is one turn.
The rail mounts under <main>, so it carries .vcp-ui-scope itself and
takes its colours from the vcp-ui tokens; its pointer-driven bar scale
and measured offsets are allowlisted in check-ui-system.mjs.

A jump that lands away from the bottom first calls
releaseChatScrollFollow() (passed in as releaseFollow), otherwise the
content-visibility messages that grow while the smooth scroll passes
them let bottom-follow pull the view back. Jumps farther than 1.5 viewports land at once and only nearby ones
animate. After landing, a ResizeObserver on the message list keeps the
question at its position before each paint. That stops when the view
has been still for 1.2s or on user wheel, touch, pointer or key input.

Long topics stay cheap. The active bar is found by binary search over
the questions, which sit in DOM order (O(log n) rects per frame; a test
checks it against the linear reference). Bars are reused by key while
older history batches prepend, and only bars whose state changed are
restyled. In the app's renderer, on 3000 synthetic messages, refresh
went from 262ms to 13ms and the cost per history batch from 619ms to
23ms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Once the reader leaves the bottom of a long conversation there is no
quick way back to the latest message. A small round arrow-down button
now sits centred 8px above the input card whenever bottom-follow is off
and the topic has messages; clicking it re-enables follow and scrolls to
the bottom.

28px outlined circle, 16px arrow, shown when follow is off and the topic has messages. The button does not
measure scroll geometry itself; it listens for the
`vcp-chat-follow-change` event from ui-helpers.js and calls
resetChatScrollFollow() + scrollToBottom({ force: true }), so follow
state keeps a single owner.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The message list used to stop at the top of the input area, so the
scrollbar ended there too. The scroller now runs to the bottom of the
window and the input area floats over its last stretch.

chat-composer-inset.js measures the input area with a ResizeObserver and
publishes its height (and the scrollbar width) as CSS variables on
<main>, then adds .vcp-chat-composer-overlay. chat-composer-inset.css
lays the input area over the scroller, pads the scroller by the
composer height plus a 20px fade, and masks the messages out behind the
composer with a short fade above it. The scrollbar column is left
unmasked. Without the script the layout is unchanged.

When the composer grows (multi-line input) a view that follows the
bottom stays pinned; a reader scrolled up is left where they are. The
turn navigator now treats the padded stretch as outside the viewport.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@RoxyAsahi

Copy link
Copy Markdown
Contributor Author

Folded into #225 as its fourth commit.

@RoxyAsahi RoxyAsahi closed this Oct 2, 2026
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