feat(web): add scroll-up lazy loading for older session messages and fix new-messages pill#893
Conversation
🦋 Changeset detectedLatest commit: cafd83a The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
commit: |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2d256df4b3
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| watch( | ||
| () => [props.hasMoreMessages, props.loadingMore], | ||
| () => { | ||
| // Re-attach the observer after a load so that a still-visible sentinel | ||
| // (e.g. the page was not tall enough to scroll) triggers another page. | ||
| observeTopSentinel(); | ||
| }, | ||
| ); |
There was a problem hiding this comment.
Attach the sentinel observer after render
When an unloaded historical session opens, ConversationPane mounts ChatPane in the loading state before the snapshot sets hasMoreMessages; this default pre-flush watcher runs before the newly v-if rendered sentinel ref exists, so observeTopSentinel() returns without observing anything. In that common path, scrolling to the top never triggers lazy loading until another prop change or a manual button click happens; make this watcher run after the DOM update (or await nextTick) before observing.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 673d5bca71
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| if (entry?.isIntersecting && props.hasMoreMessages && !props.loadingMore) { | ||
| emit('loadOlderMessages'); |
There was a problem hiding this comment.
Prevent eager pagination on initial session open
For any long session with hasMoreMessages=true, the sentinel is mounted while the pane is still at its initial top position (and even while sessionLoading is rendering), before ConversationPane's follow-to-bottom logic runs on later ticks. The observer therefore sees the sentinel immediately and calls loadOlderMessages without the user scrolling up, turning the lazy loader into an unsolicited extra page fetch on open and potentially repeating for short panes.
Useful? React with 👍 / 👎.
| await props.loadOlderMessages(props.sessionId); | ||
| await nextTick(); | ||
|
|
||
| const el2 = panesRef.value; | ||
| if (!el2) return; | ||
| const addedHeight = el2.scrollHeight - oldHeight; | ||
| el2.scrollTop = oldTop + addedHeight; |
There was a problem hiding this comment.
Suppress the latest-message pill when prepending history
When a user scrolls up and this path prepends older turns, turns.length changes while following is false, so the existing scroll watcher/mutation observer treats the prepend like new bottom content and shows the “Latest messages” pill. That pill is meant for newer messages arriving below the viewport; after loading earlier history it becomes a false prompt to jump away from the just-loaded messages.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d63b25dc72
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| function scheduleFollow(allowPill: boolean): void { | ||
| // Prepending older history changes turns.length but is not new bottom content; | ||
| // suppress the "new messages" pill until the scroll position is restored. | ||
| if (historyLoadInProgress.value) return; |
There was a problem hiding this comment.
Gate the scroll-key watcher during history loads
When older history is prepended while the user is scrolled up, props.turns.length changes and the existing scrollKey watcher still runs; because following is false, it sets showPill = true. This guard only suppresses the MutationObserver/ResizeObserver scheduleFollow path, so the “Latest messages” pill appears for a history prepend and remains after the scroll position is restored. Gate the scrollKey watcher on historyLoadInProgress as well, or clear/suppress the pill in handleLoadOlderMessages.
Useful? React with 👍 / 👎.
| const addedHeight = el2.scrollHeight - oldHeight; | ||
| el2.scrollTop = oldTop + addedHeight; |
There was a problem hiding this comment.
Avoid restoring scroll after the session changes
If the user starts loading older messages in session A and switches to session B before the request finishes, this continuation still applies oldTop + addedHeight to the shared scroll pane now showing session B. That can jump the newly selected session to an arbitrary position based on A's previous height/top. Capture the requested session (and/or element) and skip the restoration when props.sessionId has changed before these lines run.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 10a726f55f
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| const addedHeight = el2.scrollHeight - oldHeight; | ||
| el2.scrollTop = oldTop + addedHeight; |
There was a problem hiding this comment.
Restore history loads from a stable top anchor
When the user is scrolled up and an assistant response or tool output appends while loadOlderMessages is in flight, scrollHeight - oldHeight includes that bottom growth as well as the prepended page. Applying the whole delta to scrollTop shifts the viewport down by the height of the new bottom content, so reading older history jumps unexpectedly; measure an anchor near the old viewport or otherwise isolate the height inserted above it.
Useful? React with 👍 / 👎.
4673dbe to
8c72a3f
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8c72a3f12a
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| watch(scrollKey, async () => { | ||
| // Prepending older history changes this key; do not treat it as new bottom | ||
| // content and do not auto-scroll while the scroll anchor is being restored. | ||
| if (historyLoadInProgress.value) return; |
There was a problem hiding this comment.
Preserve new-message pill during history loads
When a running session receives new bottom content while an older-page request is still awaiting, scrollKey changes under historyLoadInProgress and this early return permanently drops the existing auto-scroll / “new messages” pill update; the watcher will not re-run after the flag is cleared. Users who scrolled up to load history can therefore miss that the active reply advanced during the fetch, so the suppression needs to distinguish prepended history from concurrent bottom appends.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4b21ec5c7e
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| // (e.g. the page was not tall enough to scroll) triggers another page. | ||
| // Wait for the next render tick because the sentinel is rendered by v-if | ||
| // and may not exist when this watcher first fires. | ||
| void nextTick().then(observeTopSentinel); |
There was a problem hiding this comment.
Avoid tight retries after history-load failures
When GET /messages fails while the user is near the top, loadOlderMessages leaves hasMoreMessages true and only flips loadingMore back to false. This watcher then re-observes the still-visible sentinel; the observer's initial callback immediately emits another load, so a persistent network/server error turns into a continuous request loop and warning spam until the user scrolls away. Reattach only after a successful page, or add an error/backoff state before observing again.
Useful? React with 👍 / 👎.
| let delta = 0; | ||
| if (oldAnchor) { | ||
| const newAnchor = el2.querySelector<HTMLElement>( | ||
| `.turn-anchor[data-turn-id="${attrEscape(oldAnchor.id)}"]`, |
There was a problem hiding this comment.
Preserve scroll across split assistant turns
When the page boundary cuts through an assistant/tool turn, prepending older raw messages can rebuild that first visible turn with a different ChatTurn.id because messagesToTurns uses the first message in the assistant group as the turn id. In that case this lookup cannot find oldAnchor.id, delta stays zero, and the viewport jumps into the newly inserted history instead of staying on the message the user was reading; use a fallback such as scroll-height delta or a stable message/block anchor for this boundary case.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 25e67b42d1
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| return ( | ||
| prev !== undefined && | ||
| prev.length > 0 && | ||
| next.length > prev.length && |
There was a problem hiding this comment.
Suppress the pill for same-turn history prepends
When the older page belongs to the same rendered assistant turn as the current first turn, messagesToTurns can merge the newly prepended messages into that existing first turn, changing firstId without increasing turns.length. Because this predicate requires next.length > prev.length, that prepend is treated as bottom content while the user is scrolled up, so the “Latest messages” pill appears even though no new latest message arrived. Please treat same-length first-id changes with unchanged last-turn fields as prepend-only too.
Useful? React with 👍 / 👎.
Problem
In the web chat session view, long sessions were silently truncated: the snapshot only loads the latest 100 messages, and scrolling up did not fetch older history. Additionally, the "new messages" pill was absolutely positioned at the bottom of the conversation pane, causing it to overlap the composer dock.
What changed
messagesHasMoreBySession,messagesLoadingMoreBySession) and aloadOlderMessages(sessionId)action inuseKimiWebClient.hasMoreMessages.ChatPane, anIntersectionObservertriggersloadOlderMessages, which callsGET /sessions/{id}/messages?before_id=...&page_size=50, reverses the page to chronological order, and prepends it.ConversationPanepreserves scroll position after prepending older messages.ChatPane.bottomtodockHeight + 12pxso it floats above the composer dock instead of overlapping it.loadOlderMessages.Checklist
gen-changesetsskill.