Skip to content

fix(app): keep loading history through long turns - #1842

Merged
bra1nDump merged 1 commit into
slopus:mainfrom
chphch:fix/chat-older-history-long-turn
Sep 28, 2026
Merged

bra1nDump merged 1 commit into
slopus:mainfrom
chphch:fix/chat-older-history-long-turn

Conversation

@chphch

@chphch chphch commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

Scrolling up a long chat can stop for good partway through one agent turn. With Group Tool Calls on, a turn longer than about five 100-record pages pauses automatic history paging before the prompt that opened it is reached, and on a phone the only way past that pause — Load more, or Retry after a failed page — is drawn at the top edge of the scrolled content, behind the glass header and the status bar. Expected: the list keeps paging through a long turn until its prompt arrives, as the existing test pages automatically near the oldest row, one page at a time, until the turn completes describes, and Load more / Retry sit right above the oldest row.

Why it stops

  • The invisible-page budget counts a long turn's pages as empty. fillOlder pauses after MAX_INVISIBLE_OLDER_PAGES pages that leave the oldest row's id unchanged. A collapsed work group is keyed by the message its run completes at (work-<final answer id>, see collectAgentWorkGroups) precisely so that it keeps its identity while older work arrives — so every page that lands inside the oldest turn looks empty. The existing test pages only two pages into a turn, which stays under the budget.
  • The Load more / Retry slot sits on the wrong side of the header spacer. OlderEnd renders the status slot first and the spacer second because, per its comment, "the two children are in visual order bottom-to-top … each cell is counter-flipped". FlashList counter-flips the footer as a whole (renderFooter in recyclerview/hooks/useSecondaryProps.tsx wraps it in the same inverted transform), so its children read top-to-bottom like any view and the slot ends up at the far top of the content. On a phone, SessionView runs the list under the header (chatListTopContentInset, headerOverlayHeight), so at the top of a long chat the slot is behind the header and the status bar.

Changes

  • oldestBoundary() — the far end's identity now includes the oldest collapsed group's earliest member, so a page that extends the oldest turn counts as progress. Pages of rows that never render, live rows at the newest end, and expanding a group still leave it unchanged; the existing budget tests pass as they are.
  • OlderEnd — spacer first, status slot last, so the spinner / Retry / Load more sit against the oldest row. The comment now describes what FlashList does.
  • New test: a turn whose work spans seven more pages keeps paging until its prompt arrives (on main it stops at five with Load more).

Proof

Standalone server + Expo web, one seeded session whose middle turn has 700 tool calls (1,406 stored records), Group Tool Calls on. On main the list makes 11 older-history requests — the first page, five background pages, five of its own — then stops at Load more. With this change it makes all 15 and reaches the first message.

main stops at Load more after five pages; this PR pages through to the first message

With older-history requests delayed by 3 s, the spinner sits 94 px above the oldest row on main and 6 px above it with this change:

spinner position: main vs this PR

The phone-only consequence — the slot hidden behind the glass header — was not captured on a device; it follows from the position measured above and the phone layout in SessionView.tsx.

pnpm typecheck is clean and vitest run passes 1,919 tests. sessionPresentation.test.ts fails to load (__DEV__ is not defined, from expo-modules-core) with or without this change.

Related: #1814 reported the same scenario — history stopping inside a long tool-only turn — against the paging code before ca9257a30 rewrote it.

🤖 Generated with Claude Code

Scrolling up a long chat could stop for good partway through one turn.

The chat list stops paging after five pages that bring nothing new into
view and waits for Load more. It judged "new" by the id of the oldest
row, but a collapsed work group keeps its id while the older part of its
turn arrives, so every page of a long turn counted as empty: a turn of
more than about five hundred records paused before the prompt that
opened it came into view. The oldest group's earliest member is now part
of that identity, so a page that extends the turn counts as progress.
Pages of rows that never render, and live rows at the newest end, still
count against the budget as before.

Getting past the pause was also harder than it looked on a phone. The
footer placed the spinner / Retry / Load more slot first and the header
spacer second, on the belief that the footer is drawn bottom-to-top.
FlashList counter-flips the footer as a whole, so it reads top-to-bottom
like any view, and the slot landed at the very top of the content: on a
phone, behind the header and the status bar, where the one control that
resumes paging can be neither seen nor tapped. The spacer now comes
first, so the slot sits against the oldest row.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
@bra1nDump

Copy link
Copy Markdown
Contributor

Merging, thank you :D

@bra1nDump
bra1nDump merged commit 4cf54d1 into slopus:main Sep 28, 2026
1 check passed
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.

2 participants