Repository navigation
feat(chat): turn navigator, back-to-bottom button and scroller under the composer - #225
Merged
Merged
Conversation
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>
RoxyAsahi
force-pushed
the
pr/zcode-turn-navigation
branch
from
October 2, 2026 17:48
0f7fa3f to
b315833
Compare
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>
2 of 3 tasks
RoxyAsahi
marked this pull request as ready for review
October 2, 2026 18:21
The composer overlay spanned the full width of the chat area, so the stretch of scrollbar beside it received no pointer events and could not be grabbed or clicked. Inset the composer by the measured scrollbar width. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
7 tasks done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
概述
共五个提交,可以逐个审阅:
selectTopic会重新打开底部跟随。100ms 后scrollIntoView({ behavior: 'smooth' })开始滚动,途经的content-visibility消息被渲染后变高,跟随用的 ResizeObserver 又把视图拉回底部。scrollTop一直停在最大值,命中的消息不在屏幕内。ui-helpers.js新增releaseChatScrollFollow():递增代号、取消排队中的滚到底部,并关闭跟随。滚回底部时照旧重新打开。vcp-chat-follow-change事件。enableTurnNavigator,默认打开)。关闭时导航条连同它的观察器一起卸载;renderer.js在global-settings-updated时按新值挂载或卸载。releaseChatScrollFollow()。content-visibility消息,卡住渲染进程,最后停在过时的位置。resetChatScrollFollow()+scrollToBottom({ force: true })。ui-helpers.js仍是跟随状态的唯一持有者。chat-composer-inset.js用 ResizeObserver 测量输入区,把输入区高度和滚动条宽度作为 CSS 变量写到<main>上,并加上.vcp-chat-composer-overlay。chat-composer-inset.css把输入区叠在滚动区上,给滚动区补上输入区高度加 20px 渐隐的内边距,并遮住输入区后面的消息,上沿有一小段渐隐。滚动条那一列不遮,所以滚动条能一直到窗口底部。关于跟随 API 的设计说明
目前跟随只会在用户输入时关闭(向上滚轮、触摸、拖动滚动条),主动滚动的代码没法表明自己的意图。这里没有去伪造用户输入(比如合成一个滚轮事件),而是在现有的
resetChatScrollFollow()旁边加了一个显式的释放接口:导航在滚动前释放跟随,回到底部时重新打开。如果你更倾向别的命名或形式,我可以改。测试
node --test tests/settings-schema-render.test.mjs tests/settings-value-golden.test.mjs tests/global-settings-save.test.mjs tests/conversation-turn-navigator.test.mjs tests/ui-helper-chat-scroll-follow.test.js tests/chat-back-to-bottom.test.mjs tests/chat-composer-inset.test.mjs:65 个通过check:ui-system的各步骤逐一运行,加上 chat kernel / contract 与 settings-ownership 检查),对比upstream/main的8d9a8656:没有新增失败;check:chat-contracts通过(每个提交都重新生成了chat-event-graph.json)。guard:next-delta、guard:ui-system、test:appearance-engine、guard:chat-kernel-consumers和check:settings-ownership在main上同样失败,输出完全一致。scrollTop从 22004 降到 15887。🤖 Generated with Claude Code