Skip to content

Split webviews bundle by surface (code splitting) - #5613

Merged
lawrencecchen merged 3 commits into
mainfrom
feat-webviews-code-splitting
Jun 8, 2026
Merged

lawrencecchen merged 3 commits into
mainfrom
feat-webviews-code-splitting

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Jun 8, 2026 •

Copy link
Copy Markdown
Contributor

Moves the webviews/ React build off Vite library mode + inlineDynamicImports (one 11.6 MB main.mjs) to a single-entry app build that emits per-surface chunks. main.tsx is now a slim dispatcher that dynamically imports only the active surface, so the agent session no longer ships the diff viewer and vice versa.

The diff syntax-highlighting vendor (@pierre/diffs + shiki grammars) collapses into one lazy diff-vendor chunk loaded only by the diff surface. Left fully split, shiki emits ~300 grammar files that both duplicate the vendored diff worker grammars and push the diff viewer custom scheme's per-token allowlist toward its 1024-file cap. Per-grammar lazy loading (and de-duplicating against the worker copy) is a follow-up once that cap is revisited.

This is groundwork for adding a Monaco editor surface as its own lazy chunk without bloating the shared diff bundle.

Result

Output now (was: single main.mjs 11.6 MB):

chunk size loaded by
main.mjs (entry) 0.6 KB both
vendor (React, router) 277 KB both
agentSessionSurface 360 KB agent session
diffSurface 108 KB diff viewer
diff-vendor (@pierre + shiki) 10.3 MB diff viewer (lazy)

Agent session payload: 11.6 MB -> ~0.6 KB entry + 277 KB shared + 360 KB surface.

Why this is safe

No Swift/CLI changes needed. Both serving paths already handle sibling chunks:

  • The diff viewer custom scheme (CmuxDiffViewerURLSchemeHandler) registers every emitted .js/.mjs via the CLI's recursive asset enumeration, and ESM sub-import + worker chunk loading through the scheme is already proven in production (the diff worker does 306 dynamic import()s of ./chunks/*.mjs shiki grammars).
  • The agent-session loadFileURL grants read access to the whole output directory.

File count stays well under the 1024-file allowlist cap (5 webviews files + 709 vendored pierre files).

Verification

  • bun run typecheck, bun run lint:ci, bun run verify:tanstack-router pass.
  • ./scripts/build-webviews-app.sh --check passes (deterministic chunk hashes, reproducible committed output).
  • React Compiler guard updated to scan chunks (compiler output moved out of the slim entry); reports 29 cache sites across 6 files.

🤖 Generated with Claude Code


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Note

Low Risk
Bundling and committed static assets only; runtime behavior and native loaders are unchanged, with CI guarding reproducible builds.

Overview
Replaces the webviews Vite library build (single ~11.6 MB main.mjs via inlineDynamicImports) with a single-entry app build that code-splits by surface.

main.tsx is now a small bootstrap that import()s only agentSessionSurface or diffSurface at runtime. Each surface module mounts its own UI and installs its CSS via installWebviewStyles. Rollup manualChunks hoist shared React/router into vendor, isolate @pierre/diffs + shiki into lazy diff-vendor (loaded only by the diff surface), and pin Vite’s preload helper to vendor so the agent session never statically pulls the diff vendor bundle. Chunk filenames stay stable (unhashed) to match how the diff viewer caches assets under /tmp.

The React Compiler guard now scans main.mjs and chunks/*.mjs. Committed output under Resources/markdown-viewer/webviews-app is regenerated; no Swift/CLI changes—existing custom-scheme and loadFileURL paths already serve sibling ESM chunks.

Reviewed by Cursor Bugbot for commit d36d185. Bugbot is set up for automated code reviews on this repo. Configure here.


Summary by cubic

Split the webviews bundle by surface and lazy-load each UI on demand. Added stable chunk names to avoid diff-viewer cache bloat; agent session payload drops from 11.6 MB to ~640 KB and the diff-only vendor stays in a lazy diff-vendor chunk.

  • Refactors

    • Replaced Vite library build + inlineDynamicImports with a single-entry app build that emits per-surface chunks.
    • main.tsx now dispatches via dynamic import() to agentSessionSurface or diffSurface.
    • Added Rollup manualChunks for shared vendor and diff-only diff-vendor (@pierre/diffs + shiki); pinned Vite’s preload helper to vendor so diff-vendor is only loaded by the diff surface.
    • Introduced surfaces/* and an inline style installer so each surface ships its own CSS.
    • React Compiler guard updated to scan main.mjs and chunks/*.mjs.
    • Switched to stable (non-hashed) main.mjs and chunks/*.mjs names to overwrite in place and prevent orphaned ~10 MB diff-vendor copies in the diff-viewer asset cache.
  • Migration

    • No Swift/CLI changes needed; existing loaders already handle sibling ESM chunks and workers.

Written for commit d36d185. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Refactor
    • Webview surfaces now load dynamically based on the active view instead of routing between multiple surfaces in a single render path.
    • Improved internal module organization and router type definitions for better code structure and type safety.
    • Enhanced build configuration for optimized module splitting and resource management.

Move the webviews React build off Vite library mode + inlineDynamicImports
to a single-entry app build that emits per-surface chunks. main.tsx becomes
a slim dispatcher that dynamically imports only the active surface, so the
agent session no longer ships the diff viewer and vice versa.

The diff syntax-highlighting vendor (@pierre/diffs + shiki grammars)
collapses into one lazy diff-vendor chunk loaded only by the diff surface.
Left split, shiki emits ~300 grammar files that both duplicate the vendored
diff worker grammars and push the diff viewer custom scheme's per-token
allowlist toward its 1024-file cap; per-grammar laziness is a follow-up.

Agent session payload drops from the 11.6MB monolith to a 0.6KB entry +
277KB shared vendor + 360KB surface chunk. Both serving paths already
handle sibling chunks: the diff viewer custom scheme registers every
emitted .js/.mjs, and the agent-session file load grants read access to
the whole output directory.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 8, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Canceled Canceled Jun 8, 2026 4:31am
cmux-staging Building Building Preview, Comment Jun 8, 2026 4:31am

@coderabbitai

coderabbitai Bot commented Jun 8, 2026 •

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

An error occurred during the review process. Please try again later.

📝 Walkthrough

Walkthrough

The PR refactors webviews from single-app routing to surface-specific mount functions with dynamic imports and splits the bundle into optimized chunks. A new style injection utility and surface bootstrap functions enable conditional mounting. The Vite build is reconfigured to emit stable named chunks with manual dependency splitting, and the verification script validates React Compiler optimization across all outputs.

Changes

Webview Surface Mount Refactor and Build Optimization

Layer / File(s) Summary
Style injection utility
webviews/src/surfaces/installWebviewStyles.ts
installWebviewStyles(id, styles) injects inlined CSS into the document head via a dataset-tagged <style> element.
Router type registration
webviews/src/router.tsx
WebviewsRouter type alias derived from createWebviewsRouter return type and module augmentation of @tanstack/react-router for global router typing.
Agent session surface mount function
webviews/src/surfaces/agentSessionSurface.tsx
mountAgentSessionSurface(rootElement) installs styles, applies document metadata, sets agent-session webview kind flags, creates router rendering AgentSessionApp, and mounts into root element.
Diff viewer surface mount function
webviews/src/surfaces/diffSurface.tsx
mountDiffSurface(rootElement) reads config from #cmux-diff-viewer-config DOM element, installs styles, applies appearance/title/labels, computes initial viewer status, creates router rendering App, and mounts into root element.
Entry point conditional surface selection
webviews/src/main.tsx
WebviewKind union type defined at top; entry point determines active surface via resolveWebviewKind() and dynamically imports the appropriate mount*Surface() function instead of routing both modes in a single render path.
Vite build configuration and chunk splitting
webviews/vite.config.mjs
Build refactored from lib mode to app mode: modulePreload disabled, rollup output configured for fixed main.mjs entry and stable chunks under chunks/. manualChunks splits diff/highlighting dependencies into lazy diff-vendor chunk and shared React/TanStack code into stable vendor chunk.
Build verification script for React Compiler cache
scripts/check-webviews-react-compiler.mjs
Script expands from scanning main.mjs to scanning all .mjs chunk files under webviews-app/chunks/. Sums React Compiler cache-site counts across all files and reports total count and file count.
Generated bundle output
Resources/markdown-viewer/webviews-app/chunks/installWebviewStyles.mjs
New ES module exports router builder and style injector functions produced by the optimized Vite build.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 Surfaces split and surfaces mount,
Chunks cascade down the vendor font,
Styles injected, routes aligned,
Dynamic imports intertwined,
A cleaner bundle, React-compiled with care!

🚥 Pre-merge checks | ✅ 17 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The description covers the main changes and rationale but is missing explicit testing details and does not fully follow the template structure. Clarify testing methodology: specify what manual verification was performed beyond the listed scripts and add explicit results from running the build-webviews-app.sh check.
✅ Passed checks (17 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: moving from a monolithic bundle to a code-split structure with per-surface chunks.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Actor Isolation ✅ Passed PR contains no Swift files; all changes are to webviews TypeScript/JavaScript code and build configuration. Swift actor isolation check is not applicable.
Cmux Swift Blocking Runtime ✅ Passed No Swift files were modified in this PR. All changes are in TypeScript/JavaScript webviews code. The PR explicitly states "No Swift/CLI changes required."
Cmux No Hacky Sleeps ✅ Passed No hacky sleeps, timers, polling, or wall-clock waits found in modified files. Code uses proper dynamic imports and synchronous DOM operations without race-condition workarounds.
Cmux Algorithmic Complexity ✅ Passed PR only modifies build configuration, vite.config.mjs chunk routing (O(1) string checks), and UI initialization code with no iteration over scalable collections or hot-path algorithm violations.
Cmux Swift Concurrency ✅ Passed PR contains only JavaScript/TypeScript/React changes (webviews bundle splitting); no Swift code modifications, so Swift concurrency check is not applicable.
Cmux Swift @Concurrent ✅ Passed No Swift files were modified in this PR; it contains only JavaScript/TypeScript webviews and build configuration changes, making the Swift @concurrent annotation rule inapplicable.
Cmux Swift File And Package Boundaries ✅ Passed No Swift files were modified in this PR; all changes are TypeScript/JavaScript webviews code (main.tsx, router.tsx, surfaces, vite config, mjs chunks). The custom check is not applicable.
Cmux Swift Logging ✅ Passed PR contains only TypeScript/JavaScript/build config changes; no Swift files modified. Swift logging check is not applicable to this webviews code-splitting PR.
Cmux User-Facing Error Privacy ✅ Passed No user-facing errors expose vendor names, credentials, tokens, or implementation details; all error messages are generic initialization failures, and build script output is developer-only.
Cmux Full Internationalization ✅ Passed PR contains only technical code-splitting changes with no user-facing text, new translations, or localization needs. Error messages are developer-only initialization errors exempt from i18n review.
Cmux Swiftui State Layout ✅ Passed PR contains only TypeScript/React/JavaScript webview changes; no SwiftUI code modified, so the SwiftUI state layout check is not applicable.
Cmux Architecture Rethink ✅ Passed PR contains no Swift code changes; check applies only to Swift architecture changes. All modifications are in TypeScript/React webviews and build scripts.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR contains no Swift changes—only JavaScript/TypeScript webviews build configuration and React code, so auxiliary window close-shortcut check is not applicable.
Cmux Source Artifacts ✅ Passed Committed chunks are deterministically-built artifacts required by build/release, verified by script, and part of product distribution. No temp folders or scratch directories added.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-webviews-code-splitting

Warning

Review ran into problems

🔥 Problems

Stopped waiting for pipeline failures after 30000ms. One of your pipelines takes longer than our 30000ms fetch window to run, so review may not consider pipeline-failure results for inline comments if any failures occurred after the fetch window. Increase the timeout if you want to wait longer or run a @coderabbit review after the pipeline has finished.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
{"name":"HttpError","status":500,"request":{"method":"PATCH","url":"https://api.github.com/repos/manaflow-ai/cmux/issues/comments/4645310532","headers":{"accept":"application/vnd.github.v3+json","user-agent":"octokit.js/0.0.0-development octokit-core.js/7.0.6 Node.js/24","authorization":"token [REDACTED]","content-type":"application/json; charset=utf-8"},"body":{"body":"<!-- This is an auto-generated comment: summarize by coderabbit.ai -->\n<!-- review_stack_entry_start -->\n\n[![Review Change Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/manaflow-ai/cmux/pull/5613?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)\n\n<!-- review_stack_entry_end -->\n<!-- This is an auto-generated comment: review in progress by coderabbit.ai -->\n\n> [!NOTE]\n> Currently processing new changes in this PR. This may take a few minutes, please wait...\n> \n> <details>\n> <summary>⚙️ Run configuration</summary>\n> \n> **Configuration used**: Path: .coderabbit.yaml\n> \n> **Review profile**: ASSERTIVE\n> \n> **Plan**: Pro\n> \n> **Run ID**: `2960ee86-b00a-4247-8dfa-b5a89ce5e339`\n> \n> </details>\n> \n> <details>\n> <summary>📥 Commits</summary>\n> \n> Reviewing files that changed from the base of the PR and between e3d101abc1b22c712d584f5368685499c399ffd9 and 484c204d8266dda063496d624bea8e81702a0f4f.\n> \n> </details>\n> \n> <details>\n> <summary>📒 Files selected for processing (13)</summary>\n> \n> * `Resources/markdown-viewer/webviews-app/chunks/agentSessionSurface-CPq57IBs.mjs`\n> * `Resources/markdown-viewer/webviews-app/chunks/diff-vendor-CtpyqYhA.mjs`\n> * `Resources/markdown-viewer/webviews-app/chunks/diffSurface-bV_qIRdO.mjs`\n> * `Resources/markdown-viewer/webviews-app/chunks/installWebviewStyles-BOINf57n.mjs`\n> * `Resources/markdown-viewer/webviews-app/chunks/vendor-BQYn1sxE.mjs`\n> * `Resources/markdown-viewer/webviews-app/main.mjs`\n> * `scripts/check-webviews-react-compiler.mjs`\n> * `webviews/src/main.tsx`\n> * `webviews/src/router.tsx`\n> * `webviews/src/surfaces/agentSessionSurface.tsx`\n> * `webviews/src/surfaces/diffSurface.tsx`\n> * `webviews/src/surfaces/installWebviewStyles.ts`\n> * `webviews/vite.config.mjs`\n> \n> </details>\n> \n> ```ascii\n>  _____________________________________________________\n> < The fifth dentist recommends we all floss our code. >\n>  -----------------------------------------------------\n>   \\\n>    \\   \\\n>         \\ /\\\n>         ( )\n>       .( o ).\n> ```\n\n<!-- end of auto-generated comment: review in progress by coderabbit.ai -->\n\n<!-- finishing_touch_checkbox_start -->\n\n<details>\n<summary>✨ Finishing Touches</summary>\n\n<details>\n<summary>📝 Generate docstrings</summary>\n\n- [ ] <!-- {\"checkboxId\": \"7962f53c-55bc-4827-bfbf-6a18da830691\"} --> Create stacked PR\n- [ ] <!-- {\"checkboxId\": \"3e1879ae-f29b-4d0d-8e06-d12b7ba33d98\"} --> Commit on current branch\n\n</details>\n<details>\n<summary>🧪 Generate unit tests (beta)</summary>\n\n- [ ] <!-- {\"checkboxId\": \"f47ac10b-58cc-4372-a567-0e02b2c3d479\", \"radioGroupId\": \"utg-output-choice-group-unknown_comment_id\"} -->   Create PR with unit tests\n- [ ] <!-- {\"checkboxId\": \"6ba7b810-9dad-11d1-80b4-00c04fd430c8\", \"radioGroupId\": \"utg-output-choice-group-unknown_comment_id\"} -->   Commit unit tests in branch `feat-webviews-code-splitting`\n\n</details>\n\n</details>\n\n<!-- finishing_touch_checkbox_end -->\n<!-- tips_start -->\n\n---\n\nThanks for using [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=manaflow-ai/cmux&utm_content=5613)! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.\n\n<details>\n<summary>❤️ Share</summary>\n\n- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)\n- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)\n- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)\n- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)\n\n</details>\n\n\n<sub>Comment `@coderabbitai help` to get the list of available commands and usage tips.</sub>\n\n<!-- tips_end -->"},"request":{"retryCount":3,"signal":{},"retries":3,"retryAfter":16}}}

@chatgpt-codex-connector

Copy link
Copy Markdown

💡 Codex Review

import{_ as n}from"./chunks/diff-vendor-CtpyqYhA.mjs";import"./chunks/vendor-BQYn1sxE.mjs";function o(){return document.documentElement.dataset.cmuxWebviewKind==="agent-session"||document.body.dataset.cmuxWebviewKind==="agent-session"||document.getElementById("cmux-agent-session-config")?"agent-session":"diff"}const t=document.getElementById("root");if(!t)throw new Error("Missing cmux webview root");o()==="agent-session"?n(()=>import("./chunks/agentSessionSurface-CPq57IBs.mjs"),[]).then(e=>{e.mountAgentSessionSurface(t)}):n(()=>import("./chunks/diffSurface-bV_qIRdO.mjs"),[]).then(e=>{e.mountDiffSurface(t)});

P2 Badge Keep diff-vendor out of the entry module

When agent-session.html loads main.mjs, the browser must resolve this static diff-vendor import before resolveWebviewKind() can choose the agent-session branch, so agent-session webviews still download and parse the ~10 MB diff/shiki vendor chunk even though they never render the diff surface. This defeats the intended surface split for the agent session path; move Vite's preload/runtime helper out of diff-vendor (or otherwise prevent main.mjs from statically importing it) so the diff chunk is only requested by the diff branch.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@greptile-apps

greptile-apps Bot commented Jun 8, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR replaces the monolithic Vite library build (single 11.6 MB main.mjs via inlineDynamicImports) with a single-entry app build that code-splits by surface. The compiled entry is now a 0.6 KB dispatcher that dynamically imports only the active surface chunk, reducing the agent-session payload from 11.6 MB to roughly 638 KB.

  • main.tsx dispatcher: detects surface kind at load time and import()s only agentSessionSurface or diffSurface, ensuring neither surface ships the other's code.
  • vite.config.mjs manualChunks: pins vite/preload-helper to vendor to prevent the entry from statically pulling diff-vendor; collapses @pierre/diffs + shiki into a single lazy diff-vendor chunk to stay under the 1024-file custom-scheme allowlist cap.
  • surfaces/*: each surface owns its own bootstrap (CSS inline via ?inline, router creation, React root mount), with installWebviewStyles injecting styles lazily per surface.

Confidence Score: 5/5

Safe to merge — purely frontend bundling and committed static assets; both serving paths already handle sibling ESM chunks and the code split is correctly implemented in the committed output.

The compiled main.mjs statically imports only vendor.mjs (the preload helper), confirming diff-vendor is never fetched on the agent-session page. The manualChunks vite/preload-helper pin resolves the previously flagged entry-point static pull of diff-vendor. No Swift or CLI changes, no runtime behavior change beyond which chunks load per surface.

No files require special attention. The committed chunks match the described split and the React Compiler guard has been updated to scan the new chunk layout.

Important Files Changed

Filename Overview
webviews/vite.config.mjs Drops library mode + inlineDynamicImports; adds manualChunks that correctly pins vite/preload-helper to vendor so the slim entry never statically imports diff-vendor.
webviews/src/main.tsx Slim dispatcher: resolves surface kind synchronously, then dynamically imports only the matching surface chunk; compiled output confirms only vendor.mjs is statically imported.
Resources/markdown-viewer/webviews-app/main.mjs Committed compiled entry (0.6 KB); statically imports only vendor.mjs (preload helper), dynamically imports agentSessionSurface or diffSurface — diff-vendor is NOT statically referenced here.
webviews/src/surfaces/agentSessionSurface.tsx Boots the agent-session surface: installs inline CSS, applies document metadata, creates router, mounts React root.
webviews/src/surfaces/diffSurface.tsx Boots the diff surface: reads config from DOM, applies appearance/labels/status, creates router, mounts React root; compiled chunk statically imports diff-vendor (correct, since diffSurface is itself dynamically loaded).
scripts/check-webviews-react-compiler.mjs Updated to scan all chunks/*.mjs (including diff-vendor) plus main.mjs; unnecessarily reads the 10.3 MB diff-vendor file but does not affect correctness of the compiler guard.
webviews/src/router.tsx Extracted into a shared factory called independently by each surface; module augmentation for TanStack Router type inference remains correct.

Sequence Diagram

sequenceDiagram
    participant Host as macOS Host (Swift)
    participant Entry as main.mjs (0.6 KB)
    participant Vendor as vendor.mjs (277 KB)
    participant AgentChunk as agentSessionSurface.mjs (360 KB)
    participant DiffChunk as diffSurface.mjs (108 KB)
    participant DiffVendor as diff-vendor.mjs (10.3 MB)

    Host->>Entry: loadFileURL / custom scheme
    Entry->>Vendor: static import (__vitePreload helper + React/router)
    Entry->>Entry: resolveWebviewKind()

    alt Agent Session page
        Entry->>AgentChunk: dynamic import()
        AgentChunk-->>Entry: mountAgentSessionSurface()
        Note over DiffVendor: Never loaded
    else Diff Viewer page
        Entry->>DiffChunk: dynamic import()
        DiffChunk->>DiffVendor: "static import (@pierre/diffs + shiki)"
        DiffChunk-->>Entry: mountDiffSurface()
    end
Loading

Reviews (3): Last reviewed commit: "Use stable webview chunk names to bound ..." | Re-trigger Greptile

Comment thread webviews/vite.config.mjs
// serving paths already handle sibling chunks: the diff viewer custom
// scheme registers every emitted `.js`/`.mjs`, and the agent-session file
// load grants read access to the whole output directory.
modulePreload: false,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Using modulePreload: false disables <link rel="modulepreload"> injection in HTML, but Vite still emits its __vitePreload runtime helper for dynamic imports. That helper ends up as an export of diff-vendor, causing main.mjs to statically import that 10.3 MB chunk on every page load. Using { polyfill: false } explicitly tells Vite to omit the helper entirely, removing the cross-chunk static dependency.

Suggested change
modulePreload: false,
modulePreload: { polyfill: false },

Comment thread webviews/src/main.tsx
Comment on lines +23 to 31
if (resolveWebviewKind() === "agent-session") {
void import("./surfaces/agentSessionSurface").then((surface) => {
surface.mountAgentSessionSurface(rootElement);
});
} else {
void import("./surfaces/diffSurface").then((surface) => {
surface.mountDiffSurface(rootElement);
});
const initialStatus = initialDiffViewerStatus(config, label);
document.body.dataset.filesHidden = "false";
applyDiffViewerStatusToDocument(initialStatus);
return { config, initialStatus };
}

function setupAgentSession() {
installStyles("agent-session", agentSessionStyles);
applyCodexDocumentMetadata();
document.documentElement.dataset.cmuxWebviewKind = "agent-session";
document.body.dataset.cmuxWebviewKind = "agent-session";
}

function RoutedWebview() {
if (webviewKind === "agent-session") {
return <AgentSessionApp />;
}
if (!diffRuntime) {
throw new Error("Missing cmux diff viewer runtime");
}
return <App config={diffRuntime.config} initialStatus={diffRuntime.initialStatus} />;
}

const router = createWebviewsRouter(RoutedWebview);

declare module "@tanstack/react-router" {
interface Register {
router: typeof router;
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 If the surface chunk fails to load (e.g., a missing file during a build mismatch or a load error in the custom scheme), the void import(…).then(…) call swallows the rejection silently. The previous synchronous setup would have thrown immediately. Adding a .catch ensures the failure surfaces as an unhandled error rather than leaving a blank webview with no diagnostic.

Suggested change
if (resolveWebviewKind() === "agent-session") {
void import("./surfaces/agentSessionSurface").then((surface) => {
surface.mountAgentSessionSurface(rootElement);
});
} else {
void import("./surfaces/diffSurface").then((surface) => {
surface.mountDiffSurface(rootElement);
});
const initialStatus = initialDiffViewerStatus(config, label);
document.body.dataset.filesHidden = "false";
applyDiffViewerStatusToDocument(initialStatus);
return { config, initialStatus };
}
function setupAgentSession() {
installStyles("agent-session", agentSessionStyles);
applyCodexDocumentMetadata();
document.documentElement.dataset.cmuxWebviewKind = "agent-session";
document.body.dataset.cmuxWebviewKind = "agent-session";
}
function RoutedWebview() {
if (webviewKind === "agent-session") {
return <AgentSessionApp />;
}
if (!diffRuntime) {
throw new Error("Missing cmux diff viewer runtime");
}
return <App config={diffRuntime.config} initialStatus={diffRuntime.initialStatus} />;
}
const router = createWebviewsRouter(RoutedWebview);
declare module "@tanstack/react-router" {
interface Register {
router: typeof router;
}
}
if (resolveWebviewKind() === "agent-session") {
import("./surfaces/agentSessionSurface")
.then((surface) => {
surface.mountAgentSessionSurface(rootElement);
})
.catch((err) => {
throw err;
});
} else {
import("./surfaces/diffSurface")
.then((surface) => {
surface.mountDiffSurface(rootElement);
})
.catch((err) => {
throw err;
});
}

lawrencecchen and others added 2 commits June 7, 2026 21:16
The slim entry statically imported the 10MB diff-vendor chunk because Rollup
co-located Vite's dynamic-import preload helper there, so opening an agent
session eagerly fetched the diff/shiki bundle and defeated the split. Pin the
preload helper to the always-shared vendor chunk via manualChunks so the entry
only statically imports vendor; diff-vendor is now imported solely by the diff
surface.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Content-hashed chunk names orphaned a new ~10MB diff-vendor copy in the diff
viewer's long-lived /tmp/cmux-diff-viewer-$uid/assets/cmux-webviews-app cache
on every rebuild, since nothing prunes that dir and the copy step overwrites
by size+mtime. Drop the content hash so chunk names are stable and overwrite
in place (matching the prior single main.mjs behavior). The bundle is served
via the diff viewer custom scheme and a versioned app-bundle file load, so
content-hash cache-busting is not needed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@lawrencecchen
lawrencecchen merged commit 4424644 into main Jun 8, 2026
21 checks passed
@lawrencecchen
lawrencecchen deleted the feat-webviews-code-splitting branch June 8, 2026 04:48
lawrencecchen added a commit that referenced this pull request Jun 8, 2026
Resolve the only conflict (the generated webviews bundle) by regenerating it
from the merged source with scripts/build-webviews-app.sh. main #5613 code-split
the webviews bundle into per-surface chunks; the diff surface-fill CSS now lives
in chunks/diffSurface.mjs. Verified with build-webviews-app.sh --check (exit 0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

This branch was successfully deployed

1 active deployment
Preview – cmux — d36d185f Deployed Jun 8, 2026 by vercel[bot]
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