Skip to content

Recover plugin frontend boot from a cancelled plugin list fetch - #6623

Open
ymichael wants to merge 3 commits into
mainfrom
bb/plugin-frontend-boot-sometimes-fails-with-cancel-thr_gi28trzk74
Open

ymichael wants to merge 3 commits into
mainfrom
bb/plugin-frontend-boot-sometimes-fails-with-cancel-thr_gi28trzk74

Conversation

@ymichael

Copy link
Copy Markdown
Collaborator

Human comments

What was wrong

Loading /threads/<id> redirects to /projects/<project>/threads/<id>. AppLayoutContent handled that redirect by returning <Navigate> in place of the whole layout. That unmounted every layout child, including the sidebar footer's ["plugin-list", true] query observers. When those were the last observers on an in-flight plugin list fetch, TanStack Query cancelled the fetch and reverted it, and because the query had no data yet, the revert rejected with CancelledError. If plugin frontend boot had already joined that shared fetch through fetchQuery, it got the CancelledError and boot failed. Boot is memoized, so the failure was permanent, and the failure never set the plugin frontend load-failed flag. The sidebar and composer therefore showed "No navigation/thread list/composer plugin is enabled" with no Retry until a page reload. The browser lifecycle cancelQueries that runs when the tab is hidden can cancel the same shared fetch.

What changed

  • apps/app/src/lib/plugin-frontend.ts: fetchFrontendCandidates fetches the plugin list again when fetchQuery rejects with a cancellation, so boot and live reconcile survive an observer unmount or a tab-hide cancel. Other errors, including the existing 401/403 handling, are unchanged.
  • apps/app/src/lib/plugin-frontend.ts and plugin-frontend-lazy.ts: a failed boot, or a failed plugin runtime load, sets the load-failed flag. The sidebar and composer then show their existing "couldn't load" state with Retry instead of "No … plugin is enabled". The flag is recalculated from plugin load status after each boot and live reconcile, so Retry clears it once plugins load.
  • apps/app/src/components/layout/AppLayout.tsx: the canonical thread redirect renders <Navigate> inside the layout instead of replacing it, so the redirect no longer unmounts and remounts the app shell.

How you verified

  • New tests, each of which fails without its fix:
    • plugin-frontend-reload.test.ts: a shared plugin list fetch is cancelled mid-flight, once by its last observer unmounting and once by cancelQueries.
    • plugin-frontend-boot.test.ts: a failed boot reports load-failed, and a later reconcile clears it.
    • AppLayout.test.tsx: the layout mounts once across the redirect, where it mounted twice before.
  • pnpm exec turbo run typecheck lint --filter=@bb/app passes. vitest run src/lib/plugin-frontend src/components/layout passes (23 files, 195 tests).
  • Manual check on a pnpm start:worktree server, loading /threads/<id> in headless Chrome with the plugin list response delayed 1.5s:
    • Before the fix, the first load logged plugin frontend boot failed: CancelledError and showed "No … plugin is enabled".
    • After the fix, 20 of 20 loads booted with plugins, with no layout unmount and no cancelled plugin list request. Without the delay, 10 of 10 loads booted.
    • Each load makes two plugin list requests, the same as the build without the fix.
    • With the plugin list request forced to return 400, the sidebar and composer show "couldn't load" with Retry. Clicking Retry after requests succeed again loads the navigation, the thread list and the composer.

AGENT GENERATED

🤖 Generated with Claude Code

ymichael and others added 3 commits October 29, 2026 21:13
…fetch

Loading /threads/<id> for a project thread unmounts the whole layout during
the canonical redirect. The sidebar footer's plugin-list observers go with
it, and TanStack Query cancels the in-flight list fetch. Boot's fetchQuery had
joined that fetch, so it rejected with CancelledError and left the app with
no plugin frontends. Hiding the tab mid-boot cancels it the same way.

fetchFrontendCandidates now fetches again when the shared fetch is cancelled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When boot failed before any plugin record existed, the load-failed flag
stayed false, so the sidebar and composer showed "No ... plugin is enabled"
with no way to recover short of a reload. A failed boot or runtime load now
sets the flag, so those surfaces show their failed state with Retry, and a
successful live reconcile recomputes it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AppLayoutContent replaced its whole tree with a Navigate element while
redirecting /threads/<id> to the thread's project route, so every such load
unmounted and remounted the sidebar, panels and route content, and briefly
rendered nothing. The Navigate now renders inside the layout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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