Split optional UI on demand and enforce bundle boundaries - #4512
Merged
ymichael merged 22 commits intoSep 29, 2026
Merged
Conversation
AppSidebar statically imported SidebarFooterCustomize, whose @dnd-kit imports put all four @dnd-kit packages back in boot-vendor. Load it with React.lazy behind the Customize footer action, forbid the @dnd-kit packages in the boot payload, and lower the boot limits by the verified win (61,053 bytes raw, 15,730 brotli). The thread route keeps @dnd-kit for queued-message reorder within its existing limits. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
SidebarVisibilityCustomize used a plain React.lazy with a bare "Loading…" span, so slow or failed downloads showed no header, lost the Done/Back control and Escape, and a failed import had no retry. Declare it with defineSplit (render policy, unchanged timing). The card and compact frames move into SidebarVisibilityCustomizeFrame, shared by the real editor and its placeholder, so loading and failure keep the same header, Done/Back button, Escape handling and focus target. The loading body is a muted "Loading…" that reserves the list's row height; failure is the shared one-line error with retry. Guard the implementation module from boot and SplitWorkspaceRoute in splitBoundaries, add review stories, and preload through the split in the navigation tests instead of rendering to warm it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The expanded file-change row's diff now loads through the shared split helper with render-time loading. A failed diff download stays inside its row as the shared one-line failure instead of unmounting the timeline. Loading keeps the diff card frame and header height with a muted "Loading diff…" line in place of skeleton bars. Guard TimelineFileDiffBlock.tsx out of boot and the SplitWorkspaceRoute closure, and add a Ladle split review story over real timeline rows. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Convert the terminal, browser deck, new tab, thread storage tree, and five file-preview tab wrappers from local lazy/withSuspense machinery to defineSplit with render-time loading, and remove withSuspense. - Terminal and new tab show a muted one-line loading message in the tab body; file-preview tabs keep the file-preview text skeleton so the nested LazyFilePreview load does not swap loading states. - The browser deck keeps its empty loading fallback and hides its failure line while no browser tab is showing, since the deck mounts above every other tab's content. - The thread storage tree keeps its caller-provided fallback. - Move BrowserTabLifecycleObserver out of BrowserTabDeck so the thread route no longer statically imports the deck and BrowserTabContent. SplitWorkspaceRoute drops 28,037 bytes raw and about 9 KB brotli. - Guard all five implementation modules from boot and SplitWorkspaceRoute, and add review stories using the real wrappers. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
SourceCodeHost and DiffHost now load BbSourceCode and BbDiff through defineSplit instead of module-level React.lazy. The caller's fallback is still the loading state, and still null for plugin SDK surfaces. A failed download or render stays inside the host as one red line with a retry. It no longer escapes and disables a plugin that delegates to Original. A replacement that never renders Original still does not download BB's renderer. BbDiff.tsx and BbSourceCode.tsx join splitBoundaries for boot and SplitWorkspaceRoute. Code host split review stories cover loading, failure, and loaded states. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Command rows render ANSI output through ansi-to-html and its legacy entities tables. Only expanded command details need them, so the renderer now loads through a defineSplit wrapper when the details render. The row dispatcher, full-output fetch and preview note stay eager, so auto-expanded rows never flash and the output fetch does not wait for the chunk. SplitWorkspaceRoute closure: -82,496 bytes raw, -22,383 brotli. Boot is unchanged apart from hash churn. ansi-to-html is now guarded as an on-demand package, and TerminalOutputBlock as a split boundary. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The thread route statically imported the queued-message list, which pulled @dnd-kit and the list implementation into every thread's first paint even though most threads have no queue. LazyQueuedMessagesList now owns the public queue contract (props and request types) and renders the implementation through defineSplit with render-time loading, gated on queue presence. An empty queue never downloads it. The existing data-pending summary warms the same split while queued-message details load, so data and code fetch in parallel. Loading keeps the real Queue header and count at the exact drawer height with simple skeleton bars; the data-pending summary uses the same body so the two phases do not flip. Failure is the shared one-line error inside the same card. Guard QueuedMessagesList.tsx out of boot and SplitWorkspaceRoute, and forbid the @dnd-kit packages in the SplitWorkspaceRoute closure. The route closure drops 80,109 bytes raw and 16,428 brotli. Boot grows 334 bytes raw from six extra chunk names in the dependency map. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The trigger, provider tabs, search input, picker state and every model-picker command registration stay eager. The model list, submenu, reasoning toggle group, fast-mode switch and handoff action load on first open, with trigger pointer/focus intent preloading them. Search typing and arrow-key highlighting work while the menu code loads. Guard the implementation module out of boot and SplitWorkspaceRoute. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Keep palette.open, thread.search, and plugin command registration plus open state, query text, recents, and shortcut dispatch in the eager shell. The ranked command body, settings/plugin-page actions, and PaletteShell now load through defineSplit when the palette first renders in command mode. The placeholder keeps a real search input bound to the shell's query, so typing before the body arrives is preserved, and Escape and command shortcuts work while it loads. Guard CommandPaletteBody and PaletteShell out of boot and SplitWorkspaceRoute. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ymichael
deleted the
bb/audit-project-bundle-size-and-limits-thr_icd5r2cta4
branch
September 29, 2026 22:10
ymichael
added a commit
that referenced
this pull request
Sep 29, 2026
## Human comments ## What was wrong After #4512, the shared workspace route still imported thread-only screens, Markdown's HTML parser/sanitizer, and editor extensions that are always disabled. Plugin SDK registration also imported its embedded conversation renderer. TipTap's published blockquote bundle contained another copy of ProseMirror. ## What changed - Compose the editor's enabled extensions directly, preserving rich-text gating and extension order. Patch blockquote's ESM/CJS bundles to use the existing `@tiptap/pm/state` TextSelection instead of bundled copies. - Download Markdown's HTML pipeline at workspace idle using ordinary cacheable fetches. Import it only when HTML-enabled Markdown contains `<`; coordinate fetch/import so first use does not download twice. Plain Markdown text remains visible while loading or failed; sanitization is unchanged. - Split ThreadDetailView and SDK ThreadChat. Thread URLs start the detail import alongside the workspace route; compose/plugin pages avoid it. SDK props are unchanged. Existing panel/model/palette preload policies and automatic import retries remain unchanged. - Guard the three boundaries, including the plugin runtime closure, and lower the workspace-route budgets by the measured savings. Document the download-only pattern. Production JavaScript, decimal Brotli bytes; overlapping closures are deduplicated: | Payload | Merged main | This change | | --- | ---: | ---: | | Boot | 369,221 | 369,961 | | Additional workspace route | 580,544 | 308,314 | | Initial workspace, including boot | 949,765 | 678,275 | | Initial actual thread, including boot | 949,765 | 916,608 | | Workspace plus normal background downloads, including idle HTML | 1,209,250 | 1,085,959 | These are app JavaScript closures, excluding plugin asset files, CSS, fonts and user data. Actual thread visits still need the thread code. A thread that immediately needs HTML totals 962,084 Brotli bytes, 12,319 more than the old combined route because splitting changes compression and chunk sharing; the largest benefit is on non-thread visits. Normal warmed execution excludes the 45,476 HTML Brotli bytes until needed. LOC: runtime/build +243 net; tests +236; dependency metadata +48; docs +28. The separate 6,074-line vendor patch is mostly deletions of duplicated generated ProseMirror, not 6,074 lines of application code. No review stories, fixtures, preview providers, SDK or wire contracts were added. ## How you verified - Turbo app build, typecheck and lint pass; frozen dependency install and bundle checker pass. 644 focused tests pass across targeted runs, 3 skipped. - New real cold-HTML test retains readable text while holding the actual implementation import and checks sanitization on release. It fails against the prior eager implementation. Existing HTML/media/directive tests keep their assertions, warming the implementation before testing rendered semantics. - New download-coordination tests catch duplicate first-use fetching; removing suppression fails the regression test. A real Vite build test checks emitted hashed prefetch URLs and static dependencies. Shared-chunk bundle measurement has a regression test. - Negative production build: eager imports of the three implementations fail their module guards; the HTML packages also fail their demand gates. Restored build passes. - Production Chromium: HTML/parser fetched as Fetch at idle with no V8 coverage before use; first HTML rendering executes both from disk cache with zero extra transfer bytes. Held/aborted requests preserve text and composer, show skeleton/red retry line, strip hostile handlers/scripts, and recover after reload. Cold first use now makes only the two script requests, without extra idle fetches. Desktop and phone screenshots are in the BB handoff. - Matched control: https://ymichael--6134.getbb.app/threads/thr_8bqg5t848s ; branch: https://ymichael--6135.getbb.app/threads/thr_8bqg5t848s . Both use the same isolated backend and read-only HTML response fixture. No user data was changed. Chromium only; no Safari or desktop-app claim. > AGENT GENERATED
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
Optional UI was included in startup and thread-route payloads: sidebar footer customization brought dnd-kit back into boot, queued-message reordering and ANSI output were eager, and model-menu and command-palette contents loaded before use. Existing lazy wrappers had inconsistent error handling, and hidden panel mounts downloaded optional code even while closed. Byte limits alone did not prevent those regressions.
What changed
defineSplitwith explicit loading and preload policies, shared import attempts, synchronous cached remounts, local one-line failure UI, and two bounded retries for recognized JavaScript download failures (500 ms and 1.5 seconds).docs/ui-split-loading-audit.md.Measured production payloads, in bytes (thread route is additional to boot):
No protocol or public SDK changes. Retry does not reload the page or rewrite chunk URLs. Chromium can cache a failed module URL, so repeated imports may still require a user reload. CSS-preload, initialization and render failures are not automatically retried. Legacy loaders outside
defineSplitretain their existing behavior.How you verified
pnpm exec turbo run build typecheck lint --filter=@bb/appandnode apps/app/scripts/check-bundle-budget.mjspassed during integration. Typecheck/lint and the budget check passed again after removing the review stories; lint has 206 existing warnings and no errors.Turbo-scoped focused runs passed: 204 integration tests, 13 final shared-helper tests, and 61 panel-policy tests (overlapping coverage, not an aggregate count). Tests cover cold typing, queue/data overlap, local failure containment, bounded retries and retained first-use gates.
Negative controls made imports eager and verified the real budget checker rejected protected modules/packages. Retry and demand-gate tests failed against the original implementations.
Production request capture verifies startup panel-shell warming, idle palette warming and concurrent browser/terminal/new-tab downloads on panel opening. Hidden browser/terminal views stay unmounted; xterm and BB code renderers are not downloaded just by warming. Customization, queue and output remain absent from a cold root visit. Child checks covered queue interactions and cold picker/palette input. Real request-abort checks confirmed the Chromium retry limitation.
Browser verification used headless Chromium. iOS Safari/native browser views were not reverified; the review proxy now preserves Host, restoring plugin assets/WebSockets. The shared onboarding fixture does not establish full populated-thread behavior.
Final cleanup: 88 focused app tests passed, plus 16 tests covering the CI-reported picker setup and palette anatomy failures. Plugin Guide checks passed (34 tests). Build, typecheck, lint and bundle check pass. Independent mutations showed both old and rewritten tests reject missing host fallbacks, broken sidebar controls, dropped picker queries, and unintended toggle-mount preloading. The macOS smoke failure on the previous revision occurred during dependency-cache cleanup (
ENOTEMPTY), before package tests.Common-UI warming follow-up: 170 focused tests, build, typecheck, lint and bundle guards pass. Negative controls disabling idle scheduling or panel-open warming fail their behavioral tests. Three matched localhost samples show no palette download on first opening after idle; timing details and screenshots are in the thread-storage handoff. No new byte limits were raised.
Page-load model warming: production requests confirm the model menu loads alongside the panel shell before opening. 129 focused tests, build/typecheck/lint and budget guards pass. Fixed the app-2 selection-test timeout by awaiting thread-search module initialization in test setup; a 1.3-second delayed-import experiment fails before and passes after, with the same selection assertions.