Keep older timeline pages loaded while a thread streams - #3686
Merged
Merged
Conversation
Every appended event changes the latest page's history snapshot, and the client replaced all loaded rows on any snapshot change. Messages that only existed on an older page disappeared on each event and reloaded, which flickered while a thread streamed. Keep loaded older pages when the latest page reaches the loaded tip and the server reports no later change to an older group through the new timelinePage.olderGroupsSourceSeqEnd field. Apply in-flight older pages whose cursor is still current, keep loaded order when an older page repeats rows, and anchor a group's cursor at its message row. Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
The latest page only reported changes to older conversation groups. When it cut the content of its oldest group, rows before the cut kept their loaded state, so a command that completed later stayed pending. Rename the page field to olderRowsSourceSeqEnd and include every projected row the page omitted before its returned rows: older groups, owned groups a budget cut left out, and leaves a content cut dropped from the oldest returned group. Co-Authored-By: Claude Code <noreply@anthropic.com>
SawyerHood
deleted the
bb/fix-content-flickering-issue-thr_i4ciaij4fu
branch
September 15, 2026 00:34
SawyerHood
added a commit
that referenced
this pull request
Sep 15, 2026
## Human comments ## What was wrong When a turn finished, the thread timeline always folded its work into one "Worked for" row and left only the final answer visible. `groupCompletedTurnMessages` in `@bb/thread-view` ran for every completed turn, and there was no provider declaration field or user setting to turn it off. For agents that narrate their work as they go, such as Claude Code, the narration and tool rows disappeared from view as soon as the turn ended. #3144 reports this for Claude Code turns shaped `text, tool, text, ...`: prose the user was reading while the turn ran collapses into the summary row at completion. ## What changed - **Provider default.** `bb.providers.register` accepts `completedTurnDisplay: "collapse" | "flat"`, defaulting to `"collapse"`. Host policy validates it and it is projected onto `ProviderInfo.completedTurnDisplay`. Claude Code declares `"flat"`. The field has no `experimental_` prefix, so an older server ignores it instead of refusing to load the plugin. Plugin SDK 0.4.95. - **User setting.** New general setting `providerCompletedTurnDisplay`, a map of provider id to `collapse` or `flat`. A provider without an entry uses its default. - Settings → Providers has a "Collapse finished turns" section with one switch per provider. Setting a switch to the provider's default removes the entry. - `bb settings completed-turns [provider-id] [collapse|flat|default]` lists each provider's display and where it comes from, or sets one; `default` removes the entry. - **Server.** The timeline, turn-summary details, and conversation outline routes resolve the display once per request (setting, then provider default, then `collapse`) and pass it to the `@bb/thread-view` builders. It is part of the timeline response cache key, the cursor surface, and the outline projection key, so a setting change rebuilds at the same sequence. A cursor minted under one display returns 400 under the other. A flat build keeps full turn detail, so summary pages still carry every finished-turn row. - **Client.** Timeline responses include `completedTurnDisplay`. `resolveLoadedTimelineSurfaceKey` (`@bb/client-core`) folds it into the thread timeline controller's surface key. Since #3686 a refresh keeps older loaded pages, and without this a display change left those pages in the previous display until reload. - **Docs.** `docs/configuration.md`, `docs/provider-plugin-api.md`, `docs/timeline-pagination.md`, the bb-cli skill references, the customization guide template, and the plugin-authoring references. - No server/daemon wire change; `HOST_DAEMON_PROTOCOL_VERSION` is unchanged. ## How you verified - New tests: - `packages/thread-view/test/completed-turn-summary-rendering.test.ts`: a flat finished turn keeps the rows it had while running, including a turn whose work summary detail would otherwise drop; a turn-details request in flat mode returns the ungrouped rows. Removing the flat branch or the full-detail override makes these fail. - `apps/server/test/public/public-thread-timeline-completed-turn-display.test.ts`: Claude Code threads render flat by default and Codex threads collapse; a setting change applies to the timeline, turn details, and outline at the same `maxSeq`; with several loaded pages, switching the display replaces the loaded pages and reloading older pages matches the canonical timeline. Removing the display from the cache key, the outline key, or the surface key makes these fail. - `apps/app/src/components/thread/timeline/useThreadTimelineController.test.tsx`: loaded older rows are replaced when the display changes. - Declaration validation, first-party registration defaults, `bb settings completed-turns`, and the Providers switches. - `pnpm exec turbo run typecheck` across the repo. - `pnpm exec turbo run test` for `@bb/domain`, `@get-bb/plugin-sdk`, `@bb/thread-view`, `@bb/server`, `@bb/cli`, `@bb/app`, `@bb/client-core`, `@bb/db`, `@bb/sdk`, `@bb/server-contract`, `@bb/provider-parity`, `@bb/templates`, `@bb/demo-server`, `@bb/plugin-api-map`, `@bb/provider-bridge-protocol`, and `bb-plugin-provider-claude-code`. Everything passed except two `@bb/server` timeouts under parallel load (`public-thread-data`, `timeline-in-turn-window`), which pass when run alone. On this machine the `@bb/server` run also logs `ENOSPC` watcher errors from `builtin-plugins.test.ts`, which happen on clean main too because the machine's inotify watch budget is exhausted. - `node .github/workflows/check-plugin-sdk-version.mjs` and `node packages/plugin-sdk/scripts/check-npm-version-guard.mjs` pass. - Provider corpus: row snapshots from the base commit and from this branch are identical in row content for all 340 threads. The only differences are `timelinePage.historySnapshot` and `olderCursor`, whose cursor surface now includes the display. - Manual, in a dev app: a real Claude Code turn with narration and two file reads stayed flat when it finished. Switching Claude Code to collapse in Settings → Providers folded it into "Worked for 2s", expanding that row loaded its details, and switching back removed the stored entry and survived a reload. `bb settings completed-turns` set and cleared the same setting. Fixes #3144 Fixes #3283 Related: #1656 reported the same disappear-on-completion effect for replies to mid-turn messages. 🤖 Generated with [Claude Code](https://claude.com/claude-code) > AGENT GENERATED Co-authored-by: Claude Code <noreply@anthropic.com>
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.
Human comments
What was wrong
Since #3325,
mergeLoadedTimelineWithLatestreplaces every loaded row whenever the latest page'shistorySnapshotchanges, and the snapshot includesmaxSeq, so each appended event unloads all older pages. When the previous conversation group is larger than the 1,500-event latest-page budget, it exists only on an older page. While a thread streams, each event removes it, the "Loading older messages" sentinel scrolls back into view, auto-load fetches it again, and the next event removes it again. The controller also discarded an older page still in flight whenever the snapshot moved. Separately, a page's older cursor used the group's first displayed row. Rows recorded after a request but displayed above its message, such as "Provider environment resolved", formed their own group with a higher sequence, so the first older request re-served the current group and merging it moved that row below the reply.What changed
packages/client-coremergeLoadedTimelineWithLatest: when a new latest snapshot reaches the loaded tip, loaded older pages stay and only the rows the latest page covers are replaced. It still rebuilds from the latest page when the pages do not line up, when they share no rows and leave a gap, or when the server reports a later change to a row the page omitted. On a cursor tie it keeps the walk's cursor.prependOlderTimelineRowskeeps the loaded order when an older page repeats loaded rows.useThreadTimelineController: an in-flight older page is applied when its request cursor is still the loadedolderCursor, instead of requiring an unchanged snapshot.apps/serverpagination: newtimelinePage.olderRowsSourceSeqEnd, the greatestsourceSeqEndamong rows the snapshot projected before the page's returned rows but omitted (nullwhen none). That covers older conversation groups, owned groups a size cut left out, and the leaves a content cut dropped from the page's oldest group, so a command that completes before a content cut still triggers a rebuild. A row that keeps changing before a content cut therefore rebuilds on each refresh. Leading rows recorded after a group's message now join that group, and its cursor is the message row.packages/server-contract: the field is optional intimelinePageMetadataSchema, matching the other snapshot fields. No daemon wire change, soHOST_DAEMON_PROTOCOL_VERSIONis unchanged.docs/timeline-pagination.mddescribes the new rule. This relaxes Fix timeline pagination with snapshot-bound conversation groups #3325's "a new latest snapshot replaces loaded rows" behavior for refreshes that only append to newer rows; a later change to a row the page omitted still replaces them.timeline-event-budget.test.ts. After a late append that sorts into the newest group, the merge must now equal the full canonical timeline rather than the latest page alone.How you verified
pnpm exec turbo run typecheckfor@bb/server-contract,@bb/client-core,@bb/serverand@bb/app, and earlier for@bb/cliand@bb/mobile.pnpm exec turbo run testfor@bb/client-core,@bb/server-contract,@bb/cli, and@bb/server -- test/services/threads test/provider-corpus test/routes. The provider corpus correctness walk skipped because no corpus was available.src/components/thread/timeline,src/views/thread-detailandsrc/hooks/queries, because the PWA icon check fails in this checkout.BB_FF_TIMELINE_WINDOW_EVENT_BUDGET=20and real Claude Code turns sent from the composer. A MutationObserver recorded whether the previous assistant message stayed in the DOM while a follow-up streamed. With main's client merge it left and returned 8 times within about 4 seconds, and "Loading older messages" showed in 19 samples. With this change it never left over a 30-second turn.nullthroughout.🤖 Generated with Claude Code