Skip to content

feat(web): add scroll-up lazy loading for older session messages and fix new-messages pill#893

Merged
wbxl2000 merged 10 commits into
mainfrom
feat/web-session-lazy-loading
Jun 18, 2026
Merged

feat(web): add scroll-up lazy loading for older session messages and fix new-messages pill#893
wbxl2000 merged 10 commits into
mainfrom
feat/web-session-lazy-loading

Conversation

@wbxl2000

Copy link
Copy Markdown
Collaborator

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

  • Added per-session pagination state (messagesHasMoreBySession, messagesLoadingMoreBySession) and a loadOlderMessages(sessionId) action in useKimiWebClient.
  • On first snapshot, capture hasMoreMessages.
  • When the user scrolls near the top of ChatPane, an IntersectionObserver triggers loadOlderMessages, which calls GET /sessions/{id}/messages?before_id=...&page_size=50, reverses the page to chronological order, and prepends it.
  • ConversationPane preserves scroll position after prepending older messages.
  • Added a top sentinel / "Load earlier messages" button and a loading indicator in ChatPane.
  • Fixed the "new messages" pill by dynamically setting its bottom to dockHeight + 12px so it floats above the composer dock instead of overlapping it.
  • Added i18n strings for the new button/loading text.
  • Added a unit test for loadOlderMessages.

Checklist

  • I have read the CONTRIBUTING document.
  • I have explained the problem above.
  • I have added tests that prove my feature works.
  • Ran gen-changesets skill.
  • This PR needs no doc update (web UI-only change, no CLI user docs impact).

@changeset-bot

changeset-bot Bot commented Jun 18, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: cafd83a

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@moonshot-ai/kimi-code Patch

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

@pkg-pr-new

pkg-pr-new Bot commented Jun 18, 2026

Copy link
Copy Markdown
pnpm dlx https://pkg.pr.new/@moonshot-ai/kimi-code@cafd83a
npx https://pkg.pr.new/@moonshot-ai/kimi-code@cafd83a

commit: cafd83a

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +129 to +136
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();
},
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@wbxl2000

Copy link
Copy Markdown
Collaborator Author

@codex

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +115 to +116
if (entry?.isIntersecting && props.hasMoreMessages && !props.loadingMore) {
emit('loadOlderMessages');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +499 to +505
await props.loadOlderMessages(props.sessionId);
await nextTick();

const el2 = panesRef.value;
if (!el2) return;
const addedHeight = el2.scrollHeight - oldHeight;
el2.scrollTop = oldTop + addedHeight;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 Badge 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 👍 / 👎.

@wbxl2000

Copy link
Copy Markdown
Collaborator Author

@codex

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +509 to +510
const addedHeight = el2.scrollHeight - oldHeight;
el2.scrollTop = oldTop + addedHeight;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@wbxl2000

Copy link
Copy Markdown
Collaborator Author

@codex

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +514 to +515
const addedHeight = el2.scrollHeight - oldHeight;
el2.scrollTop = oldTop + addedHeight;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@wbxl2000 wbxl2000 force-pushed the feat/web-session-lazy-loading branch from 4673dbe to 8c72a3f Compare June 18, 2026 11:02
@wbxl2000

Copy link
Copy Markdown
Collaborator Author

@codex

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@wbxl2000

Copy link
Copy Markdown
Collaborator Author

@codex

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +532 to +535
let delta = 0;
if (oldAnchor) {
const newAnchor = el2.querySelector<HTMLElement>(
`.turn-anchor[data-turn-id="${attrEscape(oldAnchor.id)}"]`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@wbxl2000

Copy link
Copy Markdown
Collaborator Author

@codex

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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 &&

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 Badge 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 👍 / 👎.

@wbxl2000 wbxl2000 merged commit d7ec056 into main Jun 18, 2026
8 checks passed
@wbxl2000 wbxl2000 deleted the feat/web-session-lazy-loading branch June 18, 2026 12:12
@github-actions github-actions Bot mentioned this pull request Jun 18, 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