Skip to content

Paint the thread view from cache while bootstrap is in flight - #6

Merged
Guitaraholic merged 2 commits into
mainfrom
fix/thread-paint-from-cache
Sep 2, 2026
Merged

Paint the thread view from cache while bootstrap is in flight#6
Guitaraholic merged 2 commits into
mainfrom
fix/thread-paint-from-cache

Conversation

@Guitaraholic

@Guitaraholic Guitaraholic commented Sep 2, 2026

Copy link
Copy Markdown

Human comments

What was wrong

Sending from new-thread calls createThread, which already writes threadQueryKey via applyCreateThreadResult. Navigate then remounts the center pane (RootComposeViewThreadDetailView). ThreadDetailViewInternal disabled useThread until GET /threads/:id?include=environment,host settled, so thread was undefined, threadQueryState was loading, and the user saw RouteLoadingSkeleton.

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/app bug, 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

  • resolveThreadDetailQueryMount enables useThread when threadQueryKey already has data.
  • Cold open with an empty cache still waits for bootstrap, so we do not add a duplicate GET /threads/:id next to the include fetch.
  • Fresh bootstrap still suppresses that duplicate GET. Stale bootstrap still refetches.
  • Environment and hosts stay gated on bootstrap; those fields are not in the create payload.

No router change. No ChatView merge. No HOST_DAEMON_PROTOCOL_VERSION bump.

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). Unifying RootComposeView and ThreadDetailView is 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) records thread-chrome-ready from the create-seeded cache while bootstrap is still in flight, then requires that mark to precede bootstrap-settled. Waiting on the include GET to paint fails this test.

  • apps/app/src/hooks/queries/thread-queries.test.tsx also asserts an empty cache does not start a second thread GET. 24 passed.

AGENT GENERATED

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