Skip to content

fix(native-chat): load older messages automatically before the reader reaches the top - #22541

Merged
brennanb2025 merged 6 commits into
mainfrom
brennanb2025/native-chat-auto-load-older
Sep 24, 2026
Merged

brennanb2025 merged 6 commits into
mainfrom
brennanb2025/native-chat-auto-load-older

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 17 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​597 $\color{#cf222e}{\Huge{\mathbf{−}}}$​68 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​529
Prod 22 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​310 $\color{#cf222e}{\Huge{\mathbf{−}}}$​158 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​152

ELI5

In a chat, scrolling up to older messages meant hitting a "Load earlier messages" button and waiting. Now the chat starts fetching older messages while you are still scrolling toward them, so by the time you get there they are usually already on screen. The button only shows up if loading actually failed, as a retry, and your messages always stay on screen when that happens.

What Changed

Before:

  • Older history loaded only when you scrolled within 80px of the very top, on an upward scroll, and only if the number of visible rows had changed since the last request.
  • The "Load earlier messages" button was always visible whenever older history existed, so you reached the top, saw the button, and watched it say "Loading…".
  • A page containing only hidden rows (for example harness-injected turns the transcript doesn't draw) left the row count unchanged, so automatic loading stopped for good.
  • In the structured chat lane, a failed older page replaced the whole conversation with "Conversation could not be loaded", with no retry, until the next reconnect.

After:

  • An invisible marker sits at the top of the transcript. An IntersectionObserver rooted at the transcript scroller watches it with a 600px top margin, so the next page is requested about a screen before the reader reaches the top. The observer is recreated whenever a page settles or paging is reset, and its first report re-checks the range. So paging continues while the marker is still in range and stops once a prepend pushes it out or history runs out. Row measurement and resizes can no longer trigger a load, and the row-count gate is gone.
  • While automatic loading works, the top of the transcript shows only a quiet role="status" line. It reads "Loading earlier messages…" if a page takes more than 200ms.
  • The retry button appears only when a page failed, or came back without moving any further back (so a host that keeps answering without progress cannot cause a request loop). Clicking it retries.
  • A failed older page never replaces the conversation, in either chat lane. The structured lane now reports the failure to the list instead of putting the whole pane into an error state.
  • Each lane publishes a paging generation that changes whenever its older-history paging is reset: in the structured lane on connect, reconnect, snapshot, reset or close; in the transcript lane on a source change, a snapshot or a file replacement. A failure belongs to one generation, so after a reconnect automatic loading resumes on its own, without a click.
  • The top row, and its observer, exist only while the transcript read is ready. A pending or errored read can't strand paging, and recovery remounts the row, which re-checks the range.
  • The top row sits inside the scroller's existing top padding instead of in the content flow, so it appearing or disappearing never moves the messages. Without this, the transcript jumped 52px when the last page landed.
  • The old scroll-offset trigger (shouldLoadEarlier and its threshold) is removed rather than kept alongside the new one.
  • Both lanes keep the page-result contract already on main (a page resolves as applied, unchanged, exhausted, superseded or failed, and concurrent callers join the page already in flight). The list reads that result; nothing rejects.
  • The live-session contract types moved to native-chat-live-session-contract.ts so the hook stays under the file-size limit.

Deliberate choices:

  • There is no separate scroll anchor captured before each load. The virtualizer's existing end anchoring keeps the reader's row in place across a prepend, and a second anchor would compete with it.
  • Treating a page that made no progress as a stop is an extra safety net, not something the current host produces. Today's host always returns progress.

Why

Paging itself is still needed. The list is virtualized, but that only limits what is drawn; the host still has to send history in bounded pages, because a whole long transcript can't cross an SSH or paired-host connection in one message. What was wrong was the trigger (too late, and able to stall), a button shown even when nothing was needed from the user, and a failure in one lane that took the whole conversation away. Observing a marker with a generous margin loads earlier and can't fire on measurement churn. Keeping failures to the list, and clearing them when the lane resets its paging, means a failure never hides your messages and never needs a click once the connection recovers.

Linked Issue

None. Reported directly while using the native chat.

Visual Proof

Before: the "Load earlier messages" button was always at the top whenever older history existed (the same button now appears only after a failure, shown in the third image).

After, running in a hidden dev app against a 1000-message transcript (captured before the failure-containment change; updated screenshots, including the structured-lane failure path, are in the QA comment below):

Loading status while a page is in flight (the page was requested 482px before the top):
v2-01-loading-status-visible.png

True start of history: no status row, no button, first message on top:
v2-02-top-of-history.png

After a failed load, the retry button appears (clicking it loaded the page and automatic loading resumed):
v2-03-failure-button.png

Testing

  • Ran on macOS in a hidden dev app with a synthetic 1000-message Claude transcript in the transcript chat lane, with both scrollTop steps and real mouse-wheel input: every page was requested before reaching the top, the button never appeared while loading worked, the reader's visible row moved 0px across every prepend including the final page, and paging stopped at the first message. Renaming the transcript mid-scroll made the retry button appear with the messages still shown; restoring it and clicking retry resumed automatic loading.

  • NativeChatMessageList.older-history.test.tsx (15 tests): loads when the marker is in range; keeps paging while in range, including after a page of hidden rows; stops when out of range or out of history; stops observing while a page loads; does not strand when the lane drops a page; a hidden pane doesn't observe; delayed status label; a failed page shows the retry and a retry re-enables; a page with no progress stops and offers the retry; a new paging generation resumes automatically with no click; no top row while the read isn't ready, and a re-check once it is; the reader's position holds across a prepend and when the last page removes the row.

  • Lane tests: a structured older page that fails (for any reason) reports failed and leaves the conversation, with no pane error. A structured snapshot starts a new paging generation, as does a transcript snapshot. The structured session hands the list the page result and the generation.

  • Each new test was checked to fail with its fix removed.

  • pnpm tc, the full native-chat test folder (183 files, 1851 tests), oxlint on every changed file, check:code-quality:changed and check:react-doctor:changed pass.

  • Not tested: SSH or paired remote hosts (renderer-only change; the same page reads are used unchanged), Linux, or Windows.

  • I manually tested these changes locally

  • Automated tests added/updated, or explained why not below

AI Disclosure

Review

Known and left alone:

  • If the reader is parked at the very top (scrollTop 0) when a page lands, which mostly happens right after clicking the retry button, the visible row can move 12–16px. That comes from the virtualizer only correcting size changes of rows that end above the viewport. Fixing it means changing that shared correction rule.
  • In the structured lane, once a reader has paged far back on a busy remote session, a page can lose its race with live trimming three times in a row and come back as no progress. The retry button then appears even though nothing failed; clicking it works.
  • Clicking the retry button replaces it with the status line, so keyboard focus returns to the page.

Agent skill upstream boundary

  • Not applicable, or this change follows docs/reference/agent-skill-sharing-upstream-boundary.md and copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

  • Security: no new inputs or IPC.
  • Cross-platform: IntersectionObserver is available in every Electron renderer. If it is missing, the list falls back to the button.
  • Remote SSH / paired hosts: no wire changes. The same page reads are used; only how the renderer reacts to a failed page changed.
  • Mobile: not affected.
  • Performance: fewer checks than before (one observer, recreated once per page or paging reset, instead of work on every scroll event). The paging generation changes only on connect, snapshot, reset or file replacement, never while streaming.

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

… reaches the top

The transcript paged older history only on an upward scroll within 80px of
the top, and only when the visible row count had changed since the last
request, so readers hit a "Load earlier messages" button and waited. A page
that landed only hidden rows left the count unchanged and stalled paging.

A sentinel above the first row is now observed by an IntersectionObserver
rooted at the transcript scroller with a 600px top margin. The observer is
recreated each time a page settles, so it keeps paging while the sentinel
stays in range and stops once a prepend pushes it out or history runs out.
The virtualizer's existing prepend anchoring keeps the reader's row in place.

Both lanes' load calls now reject when a page does not land (error result,
rejected read, host reset, exhausted re-anchoring). A rejection stops
automatic paging and shows the manual button; a successful retry resumes it.
While automatic paging works, the top shows only a polite status line.
…ands

The older-history row sat in the transcript column above the window, so the
final page, which takes that row away, removed its height and the column gap
from above the window and every row jumped up by that much.

The row now sits out of flow, absolutely positioned inside the scroller's
existing top padding, so its presence never changes the content height or
the window's scroll margin. The sentinel keeps the same place relative to the
top of the transcript, so prefetch is unaffected, and the button stays in the
padding band above the first row.
@coderabbitai

coderabbitai Bot commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: 8d02e374-e8bc-4ed8-b428-3f148a90f5b6

📥 Commits

Reviewing files that changed from the base of the PR and between 9335d9d and 2f87f12.

📒 Files selected for processing (36)
  • src/renderer/src/components/native-chat/NativeChatMessageList.older-history.test.tsx
  • src/renderer/src/components/native-chat/NativeChatMessageList.stream-render.perf.test.tsx
  • src/renderer/src/components/native-chat/NativeChatMessageList.task-list-frames.test.tsx
  • src/renderer/src/components/native-chat/NativeChatMessageList.test.tsx
  • src/renderer/src/components/native-chat/NativeChatMessageList.tool-stream-cost.test.tsx
  • src/renderer/src/components/native-chat/NativeChatMessageList.tsx
  • src/renderer/src/components/native-chat/NativeChatMessageList.turn-fold.test.tsx
  • src/renderer/src/components/native-chat/NativeChatMessageList.turn-history.test.tsx
  • src/renderer/src/components/native-chat/NativeChatMessageList.turn-indicator.test.tsx
  • src/renderer/src/components/native-chat/NativeChatMessageList.turn-timing.test.tsx
  • src/renderer/src/components/native-chat/NativeChatMessageList.windowing-test-support.tsx
  • src/renderer/src/components/native-chat/NativeChatResolvedView.tsx
  • src/renderer/src/components/native-chat/NativeChatStructuredSession.test-harness.tsx
  • src/renderer/src/components/native-chat/NativeChatStructuredSession.test.tsx
  • src/renderer/src/components/native-chat/NativeChatStructuredSession.tsx
  • src/renderer/src/components/native-chat/native-chat-autoscroll.test.ts
  • src/renderer/src/components/native-chat/native-chat-autoscroll.ts
  • src/renderer/src/components/native-chat/native-chat-live-session-contract.ts
  • src/renderer/src/components/native-chat/native-chat-windowing-test-harness.tsx
  • src/renderer/src/components/native-chat/structured-agent-session-read-owner.ts
  • src/renderer/src/components/native-chat/use-native-chat-live-session-pending.test.ts
  • src/renderer/src/components/native-chat/use-native-chat-live-session.test.ts
  • src/renderer/src/components/native-chat/use-native-chat-live-session.ts
  • src/renderer/src/components/native-chat/use-native-chat-older-history-autoload.ts
  • src/renderer/src/components/native-chat/use-native-chat-retained-session.test.ts
  • src/renderer/src/components/native-chat/use-native-chat-transcript-scroll.test.tsx
  • src/renderer/src/components/native-chat/use-native-chat-transcript-scroll.ts
  • src/renderer/src/components/native-chat/use-structured-agent-session-read.test.tsx
  • src/renderer/src/components/native-chat/use-structured-agent-session-read.ts
  • src/renderer/src/components/native-chat/use-structured-agent-session.ts
  • src/renderer/src/i18n/locales/en.json
  • src/renderer/src/i18n/locales/es.json
  • src/renderer/src/i18n/locales/fr.json
  • src/renderer/src/i18n/locales/ja.json
  • src/renderer/src/i18n/locales/ko.json
  • src/renderer/src/i18n/locales/zh.json
💤 Files with no reviewable changes (3)
  • src/renderer/src/components/native-chat/native-chat-autoscroll.ts
  • src/renderer/src/components/native-chat/native-chat-autoscroll.test.ts
  • src/renderer/src/components/native-chat/use-native-chat-transcript-scroll.test.tsx
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/renderer/src/i18n/locales/en.json
  • src/renderer/src/i18n/locales/es.json
  • src/renderer/src/i18n/locales/fr.json
  • src/renderer/src/i18n/locales/ko.json
  • src/renderer/src/i18n/locales/zh.json
  • src/renderer/src/i18n/locales/ja.json

Included review availability: Your plan provides up to 10 included reviews per hour; 8 remain after this review.


📝 Walkthrough

Walkthrough

Older-history loading now uses an intersection-observed sentinel instead of scroll-event checks. The live-session state exposes a paging generation that changes on transcript resets and read invalidation. The transcript displays a delayed loading status during automatic paging and offers a manual retry when automatic paging is unavailable or a page fails. Tests cover paging, retries, read states, and viewport position during prepends. Loading-status translations were added for six locales.

Priority: ➖ Normal

Merge Risk: ⚪ Minimal · up to 2f87f

Stale paging failures do not stop automatic loading for a replacement transcript. No actionable merge-blocking issue remains after normal checks.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 35 functions across 31 files. (6 skipped:… Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ⚠️ Warning The description explains the user-visible behavior, implementation, rationale, testing, visual proof, risks, and checklist. However, the required Linked Issue section contains “None,” although the tem… Add the issue reference in the Linked Issue section, for example “Fixes #1234.” If no issue exists, create or link one before merging.
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely describes the primary change: automatic loading of older native-chat messages before the reader reaches the top.
Full details: Docstring Coverage

Explanation

Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 35 functions across 31 files. (6 skipped: 6 unsupported.)

Full details: Description check

Explanation

The description explains the user-visible behavior, implementation, rationale, testing, visual proof, risks, and checklist. However, the required Linked Issue section contains “None,” although the template states that an issue link must always be provided.

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: f050dd84-9da4-43af-b24b-cf7e81582956

📥 Commits

Reviewing files that changed from the base of the PR and between 519bde8 and 9335d9d.

📒 Files selected for processing (26)
  • src/renderer/src/components/native-chat/NativeChatMessageList.growth-windowing.test.tsx
  • src/renderer/src/components/native-chat/NativeChatMessageList.older-history.test.tsx
  • src/renderer/src/components/native-chat/NativeChatMessageList.tool-stream-cost.test.tsx
  • src/renderer/src/components/native-chat/NativeChatMessageList.tsx
  • src/renderer/src/components/native-chat/NativeChatOlderHistoryRow.tsx
  • src/renderer/src/components/native-chat/NativeChatStructuredSession.test-harness.tsx
  • src/renderer/src/components/native-chat/NativeChatStructuredSession.test.tsx
  • src/renderer/src/components/native-chat/NativeChatStructuredSession.tsx
  • src/renderer/src/components/native-chat/native-chat-autoscroll.test.ts
  • src/renderer/src/components/native-chat/native-chat-autoscroll.ts
  • src/renderer/src/components/native-chat/native-chat-windowing-test-harness.tsx
  • src/renderer/src/components/native-chat/structured-agent-session-read-owner.ts
  • src/renderer/src/components/native-chat/use-native-chat-live-session-visibility.test.ts
  • src/renderer/src/components/native-chat/use-native-chat-live-session.test.ts
  • src/renderer/src/components/native-chat/use-native-chat-live-session.ts
  • src/renderer/src/components/native-chat/use-native-chat-older-history-autoload.ts
  • src/renderer/src/components/native-chat/use-native-chat-transcript-scroll.test.tsx
  • src/renderer/src/components/native-chat/use-native-chat-transcript-scroll.ts
  • src/renderer/src/components/native-chat/use-native-chat-transcript-window.ts
  • src/renderer/src/components/native-chat/use-structured-agent-session-read.test.tsx
  • src/renderer/src/i18n/locales/en.json
  • src/renderer/src/i18n/locales/es.json
  • src/renderer/src/i18n/locales/fr.json
  • src/renderer/src/i18n/locales/ja.json
  • src/renderer/src/i18n/locales/ko.json
  • src/renderer/src/i18n/locales/zh.json
💤 Files with no reviewable changes (3)
  • src/renderer/src/components/native-chat/native-chat-autoscroll.test.ts
  • src/renderer/src/components/native-chat/native-chat-autoscroll.ts
  • src/renderer/src/components/native-chat/use-native-chat-transcript-scroll.test.tsx

Included review availability: Your plan provides up to 10 included reviews per hour; 4 remain after this review.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ No new issues found.

Reviewed changes

This run read the full 26-file diff for both commits and independently checked three invariants: the out-of-flow layout/scroll-margin math, the observer lifecycle, and rejection propagation.

  • Automatic paging via a sentinel: new useNativeChatOlderHistoryAutoload watches a top sentinel with a 600px prefetch margin, recreating the observer each time a page settles so paging continues while in range; this replaces the removed shouldLoadEarlier scroll-offset/row-count trigger.
  • Retry-only button: new NativeChatOlderHistoryRow renders the sentinel, a delayed role="status" line, and shows the manual Button only after a page fails.
  • Failures now propagate: loadEarlier (live lane) and loadOlder (structured read owner, new OlderPageOutcome) reject when a page does not land; the list's loadPage is the single consumer and maps rejection to the retry state.
  • Layout fix: the older-history row is positioned out of flow so the final page no longer shifts the reader.
  • Tests / i18n: new older-history suite and updated lane tests; loadingEarlierMessages added to all six locales.

The observer cannot stall in range, double-fire, or leak a failure across sessions, and no production call site drops the new rejections. The 12–16px shift the author documents at scrollTop 0 is the pre-existing virtualizer size-correction rule, correctly left out of scope.

Technical details
# Verification notes

## Layout / scroll margin
- `NativeChatMessageList.tsx:331-337` — the row is a sibling of the padded wrapper, so it is never an
  ancestor of the sizer; `nativeChatScrollOffsetWithin` walks ancestors only, so `scrollMargin` is
  unaffected when `hasMore` flips. Row `h-10` abuts the wrapper `pt-10` gutter.

## Lifecycle
- Both lanes set their loading flag before awaiting, so `shouldObserve` toggles
  `true → false → true` on every real settle, recreating the observer. `inFlightRef` guards the observer
  and manual paths. A `historyKey` of `${agent}:` (null sessionId) can never record a failure because no
  lane loads without a session id.

## Rejections
- `use-native-chat-older-history-autoload.ts:55-62` is the only production invocation of `loadEarlier` /
  `loadOlder` and attaches an onRejected handler; `loadEarlierManually` returns void.

ℹ️ Nitpicks

  • NativeChatMessageList.windowing-test-support.tsx:26 still describes the "load earlier" block as sitting in flow above the spacer. The sibling harness (native-chat-windowing-test-harness.tsx) was updated for this change, but this one was not — worth a one-line comment refresh so the two harnesses don't describe different layouts.

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

…doned mid-flight

A reconnect snapshot, replacement, or hide ends the lane's loading while its
read is still outstanding. The auto-load latch stayed held until that read
settled, so the recreated observer's report was dropped and paging stalled
with no button. Release the latch once the lane reports loading, and have the
transcript lane resolve rather than reject when a superseded read fails, so a
replaced connection cannot switch the current transcript to the retry button.
The older-history autoload kept its own "page in flight" latch beside each
lane's guard. It could not tell a request the lane had not yet reported from one
the lane abandoned before rendering it (a reconnect snapshot in the same batch),
so a read that never settled could stall paging with no retry button.

The transcript lane now dedupes synchronously on the epoch its page is in flight
for, so an epoch bump abandons it with no extra reset path; the structured owner
already dedupes synchronously. The autoload hook drops its latch and the effect
that cleared it.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ No new issues found.

Reviewed changes

This run re-reviewed the two commits pushed since the prior pullfrog review, which move ownership of the in-flight older-history page out of the list and into each chat lane.

  • Each lane now owns the in-flight page: the list-level inFlightRef latch is gone from use-native-chat-older-history-autoload.ts; the transcript lane dedupes synchronously via olderPageEpochRef and the structured owner via snapshot.loadingOlder, so a second request before loadingEarlier renders is a no-op instead of a duplicate read.
  • Stale rejections are distinguished from real failures: loadEarlier in use-native-chat-live-session.ts became async and rethrows only when !isStale(), so a read abandoned by a reconnect snapshot no longer records a failure against the new transcript; a genuine failure still rejects and stops auto-load.
  • Structured owner refactor: readOlderPage returns OlderPageOutcome (landed/not-landed/stopped) and loadOlder rejects on a page that did not land — including a host reset — so automatic paging stops rather than looping.
  • Tests: new lane-level cases cover double-call dedup, stale-rejection swallowing, and continued paging after a hung read is abandoned by a snapshot.

Both lanes set their dedup flag before the first await, and each finally only clears a latch it still owns, so removing the list-level latch cannot double-request an equivalent page. I traced the observer, manual-retry, and settle paths in both lanes and found no reachable regression.

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

…sation

Main now resolves older-page reads with an explicit result (applied,
unchanged, exhausted, superseded, failed) and joins concurrent callers to
the page already in flight. This takes that contract for both chat lanes in
place of this branch's reject-on-failure one, and the autoload hook reads
the result instead of a rejection.

A failed older page in the structured lane no longer puts the whole pane in
an error state: the conversation stays on screen and the list offers its
retry. Each lane now publishes an older-history generation that changes
whenever paging is reset (structured: open, snapshot, reset, dispose;
transcript: source swap, snapshot, replacement). The list's failure belongs
to one generation, so a reconnect or reset re-enables auto-loading without
a click, and a new generation re-checks the range.

Auto-loading also stops, offering the manual load, on a page that made no
progress, so a host that keeps answering without moving the oldest row
cannot drive a request loop. The older-history row and its observer exist
only while the read is ready, so a pending or errored read never strands
paging; recovery remounts the row and re-checks.

The live-session contract types move to their own module to keep the hook
under the file-size limit.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ℹ️ No critical issues — one minor test-contract suggestion inline.

Reviewed changes

This run re-reviewed the single commit pushed since the prior pullfrog review (8df8cf1), a merge of origin/main whose conflict resolution re-adopts main's older-page result contract across both chat lanes.

  • Result contract replaces reject-on-failure: loadEarlier/loadOlder now resolve NativeChatOlderPageResult; the autoload hook maps 'failed'/'unchanged' to the retry state and stops paging.
  • Per-lane older-history generation: both lanes publish olderHistoryGeneration, the list keys failures on it, so a reconnect/reset re-enables auto-load without a click.
  • Structured lane no longer errors the pane: a failed older page keeps the loaded conversation and offers the list's retry.
  • Row gated on a ready read: showOlderHistory = hasMore && readPhase === 'ready', so a pending/errored read never strands paging.
  • Contract module extracted: live-session types move to native-chat-live-session-contract.ts, re-exported from the hook.

I traced both lanes' resolution paths (each catches to 'failed'; a superseded read maps to 'superseded') and confirmed no production caller can receive a rejection, so the autoload's .catch-less .then cannot leak an unhandled rejection. The four older-history/lane test files pass (71 tests).

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

…on reaching the list

The structured session test still fed a rejection into loadEarlier, a contract
neither lane has any more. Assert the resolved page result and the paging
generation the list needs to stop and re-arm auto-loading.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ No new issues found.

Reviewed changes

This run re-reviewed the single commit pushed since the prior pullfrog review (8df8cf1), a test-only change to the structured-session list-passthrough test.

  • List-passthrough test now matches the resolved-result contract: loadOlder resolves 'failed' and the assertion uses resolves.toBe('failed') instead of the retired .rejects, and it additionally pins olderHistoryGeneration: 3 reaching messageListProps.session, so the test now guards both the page result and the paging generation the list needs.

The prior review's inline thread is resolved: 2f87f12 changes no production code, and NativeChatStructuredSession.test.tsx passes (18 tests).

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@brennanb2025

Copy link
Copy Markdown
Contributor Author

Tested commit 8df8cf1.

The running app was this worktree. The window identity reported branch brennanb2025/pr22541-grok-qa and worktree pr22541-grok-qa. The dev server returned the native chat message list as JavaScript, and that source contains olderHistoryGeneration and showOlderHistory. This worktree's package.json was served (HTTP 200) and a sibling worktree's package.json was refused (HTTP 403).

A. Transcript lane, happy path — PASS

A 1000-message Claude transcript was scrolled from the latest 300 messages up to the first one, both by setting the scroll position and by real wheel events. Each older page (300, then 500, 700, 900, then 1000) arrived while the reader was still well above the top. The row on screen did not move when older messages were added, including the last page (measured drift 0). The "Load earlier messages" button never appeared while those reads worked. At the true top the first line is Q0001 and the loading row is gone.

The "Loading earlier messages…" line only shows after a read has been running for about a fifth of a second, so a fast local read never flashes it. One read was held for 1.5 seconds to photograph that line. The messages stayed on screen under it, and the retry button did not appear.

Top of the transcript after scrolling

Same top after wheel scrolling

Loading line while an older page is still being read

B. Transcript lane, failure and recovery — PASS

Renaming the transcript away (and, separately, removing its read permission) left the messages already on screen in place. The chat showed "Load earlier messages" above the first row, not on top of it, and did not replace the conversation with "Conversation could not be loaded." It did not keep retrying on its own for the 1.6 seconds it was watched.

Putting the file back while that button was showing resumed automatic loading by itself, so the button was often gone before a click could land. One click that did land stepped the loaded window forward, and then a fresh read of the restored file trimmed the window back to the latest 300 messages. Scrolling after that loaded further pages with no retry button. Reopening the chat with the file present also pages on its own.

Loaded messages stay, with a retry button, after the transcript read fails

C. Structured chat, failed older page — PASS

A structured Claude chat with 340 history lines was opened (Chat UI, structured chat, and open-agent-tabs-in-chat were on, and the app was restarted after that). Forcing the older-page history read to fail left the conversation on screen and showed "Load earlier messages." It did not replace the chat with "Conversation could not be loaded," and it did not retry on its own. In one scroll the loaded history started at line 41; in the shot below the fault was on while the chat was still filling, so it stopped at line 141 with the same button and the same messages still visible. After the fault was removed, clicking the button loaded line 1 and the button went away.

Structured chat stays up and offers retry after an older page fails

After retry, the structured chat is at the first line

@brennanb2025

Copy link
Copy Markdown
Contributor Author

Review status: ready for merge review

Head: 2f87f12d17b. This PR is up to date with main (merged in, no conflicts) and is mergeable.

What changed during review

  • Paging could stall with no button when a reconnect or hide dropped a page mid-read. The list kept its own "a page is loading" lock beside each lane's, and the lock could strand. Fixed by making the lane the only owner of the page in flight. That now uses main's result contract, where concurrent callers join the page already in flight.
  • A failed older page no longer replaces the conversation. In the structured chat lane, a failed older page used to put the whole pane on "Conversation could not be loaded" with no retry (this also happens on main). Now the messages stay on screen and the retry button appears at the top, in both lanes.
  • Failures clear themselves when the connection recovers. Each lane publishes a paging generation that changes on reconnect, snapshot, reset or transcript replacement. A failure belongs to one generation, so automatic loading resumes after a reconnect without a click.
  • No request loop on a stuck host. A page that comes back without moving any further back stops automatic loading and shows the retry, instead of being asked for again forever.
  • No stranded paging on a pending or errored read. The top row and its observer exist only while the read is ready, and recovery re-checks the range.

Validation

  • Three review passes. The last one came back clean.
  • Readiness checklist run at the start and the end: no blocking items.
  • pnpm tc, the full native-chat test folder (183 files, 1851 tests), oxlint on every changed file, check:code-quality:changed and check:react-doctor:changed all pass.
  • Each new test was confirmed to fail with its fix removed.
  • Hidden-window Electron QA on 8df8cf1e36, which has the same product code as the head: fix(native-chat): load older messages automatically before the reader reaches the top #22541 (comment)
    • Transcript lane: happy path, plus failure and recovery.
    • Structured lane: an older page forced to fail keeps the conversation on screen and offers a retry, and the retry loads the first message.
    • All PASS, with screenshots.

Known, not addressed here

  • A reader parked at the very top when a page lands (mostly right after clicking retry) can see a 12–16px shift. That comes from the shared virtualizer size-correction rule.
  • On a busy remote structured session, a page that loses its race with live trimming three times shows the retry even though nothing failed. One click recovers.
  • Replacing the transcript file resets the loaded window to the latest 300 messages. That is existing behavior for a replaced file.
  • Not tested live: SSH or paired remote hosts (renderer-only change; the page requests are unchanged), Linux, Windows.

@brennanb2025
brennanb2025 merged commit 1985c1b into main Sep 24, 2026
30 checks passed
zpyoung added a commit to zpyoung/orca that referenced this pull request Sep 28, 2026
The replayed stablyai#22541 contract makes loadEarlier return the page promise; awaiting
it inside act() hung on reads the test resolves later, and the hang cascaded
into 22 later cases. Uses upstream's own void-call form.
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