Paint the thread view from cache while bootstrap is in flight - #6
Merged
Conversation
Create already writes threadQueryKey, but ThreadDetailView disabled useThread until GET /threads/:id?include=environment,host settled, so the first paint after send was a skeleton. Read the cached thread immediately and keep the duplicate GET suppressed until bootstrap finishes.
Phase log records thread-chrome-ready while GET /threads/:id is still in flight, then requires that mark to precede bootstrap-settled.
This was referenced Sep 2, 2026
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
Sending from new-thread calls
createThread, which already writesthreadQueryKeyviaapplyCreateThreadResult. Navigate then remounts the center pane (RootComposeView→ThreadDetailView).ThreadDetailViewInternaldisableduseThreaduntilGET /threads/:id?include=environment,hostsettled, sothreadwas undefined,threadQueryStatewasloading, and the user sawRouteLoadingSkeleton.The same wait happens on any later open that already had the thread in React Query (back to a thread you just left).
This is an
apps/appbug, not Electron-only. The desktop shell hosts the same tree.Related: get-bb/bb#1303 (thread open fans out ~19 requests; first paint waits on that work). There is no get-bb issue specifically about the new-thread remount / skeleton.
What changed
resolveThreadDetailQueryMountenablesuseThreadwhenthreadQueryKeyalready has data.GET /threads/:idnext to the include fetch.No router change. No ChatView merge. No
HOST_DAEMON_PROTOCOL_VERSIONbump.Why this way
T3 paints a thread from local state without throwing the view away. bb keeps compose and thread as two components and a real URL change; we are not copying T3’s router.
The create mutation already seeded the thread. The skeleton was us refusing to read it.
We considered keeping both compose and thread mounted (hidden). That leaves two
SecondaryPanelLayouts and can mean two native browser views (related get-bb get-bb#2298). UnifyingRootComposeViewandThreadDetailViewis a later identity change. This PR only unblocks first paint from cache we already own.Prefetching timeline/queue/approvals with bootstrap is a separate cut (Datahazed/bb#3, get-bb/bb#2935).
Benefit to bb
Web and desktop. Default preference is navigate-to-thread after create. After send, the user should see thread chrome (title, composer, empty timeline frame) immediately instead of a route skeleton, then fill timeline as bootstrap returns. Revisiting a cached thread is the same. Plugin API and daemon protocol are unchanged, so enrolled machines do not need an update.
Harvest
Independent of #7/#8. Safe to open on get-bb as-is. Copy this body. Pair with #3 if both land: cache paints chrome, sidecars fill without a waterfall.
Decision record: docs/perf-control-plane.md on this fork (PR #9).
How you verified
Phase-order test (
apps/app/src/test/perf-phase.ts) recordsthread-chrome-readyfrom the create-seeded cache while bootstrap is still in flight, then requires that mark to precedebootstrap-settled. Waiting on the include GET to paint fails this test.apps/app/src/hooks/queries/thread-queries.test.tsxalso asserts an empty cache does not start a second thread GET. 24 passed.