gallery: the mock app, cmux NEXT's whole window on one URL, with a perf harness

Merged
TE
2d ago
+1545-21

What

The gallery's mock app: cmux NEXT's whole window on one URL, webviews/src/gallery/app.html. It shows the sidebar, the tab strips and each pane's real web surface (agent pane, transcript, composer, menus, Session Info, New Tab, Home) on the in-page mock acpmux host and gallery fixtures. The native chrome around them is approximated.

Leo (2026-11-04): "I want the mock web view of the app to be legit like 'oh we can see the whole unified piece and it doesn't lag the f*** out of my computer/browser when I open it' and stuff".

Every condition is a URL key, and adding an axis is one row in app/axes.ts (the table is in ENTRIES.md, "The mock app"):

/gallery/app.html?fixture=agent-pane.turn-error-card/disk-full&density=compact&theme=Catppuccin%20Mocha
/gallery/app.html?layout=two&fixture2=app.window/new-tab&colorScheme=light
/gallery/app.html?windows=pro14,air13,1280x800&script=streaming
/gallery/app.html?script=tools&cpu=4&slot=custom-element
  • Fixture, layout, window size, several sizes (fixture, fixture2, layout, window, windows, zoom, sidebar, chrome).
  • Density and scale, theme and customization (density, scale, interfaceScale, fontFamily, fontSize, theme, colorScheme, accent, background, locale, reducedMotion, highContrast). These are the stage's existing env keys, passed to every pane.
  • Dynamic scripts (script=streaming|tools|error, loop=0, from frame/scripts.ts). A variant's chat comes alive on a loop through the pane's real receive, and the script pauses while the page is hidden.
  • Simulated CPU contention (cpu=N). The page gets 1/N of the main thread. The harness below uses CDP throttling instead.
  • Extension slot (slot=custom-element). A registry.js-style renderer that draws a shadow-DOM custom element, registered through the pane's real cmuxAcpmuxRegistry.
  • Ready signal for the UI judge: data-mock-app-ready="1" on <html>, plus a {type: "cmux-mock-app", status: "ready"} message to the parent and webkit.messageHandlers.cmuxPreview.

Changes that make it open fast:

  • Per-entry loading. A new virtual:cmux-gallery/entry-index module maps each entry id to its file, so a stage frame loads only its own entry and no longer evaluates every *.gallery.ts. If the index is stale, the frame falls back to the full store. Saving a gallery file updates the index without a full reload.
  • The mock app itself is plain DOM with no framework. Windows that are offscreen mount only when they scroll in, and a resize re-lays the panes out in place without reloading.

scripts/gallery-matrix/app-perf.ts (with app-perf-budgets.json) measures each scenario in Chromium: first frame, ready, long tasks after ready, idle CPU (CDP TaskDuration over a 5 s window), idle rAF and timers per second, heap, DOM nodes, and CLS after first paint. It prints ::warning for a budget miss and exits 0 unless --strict is passed.

This is not a CI gate. The brief asked for "a CI budget that warns", but the repo CLAUDE.md forbids new CI gates and budgets (Lawrence, 2026-10-23). So the budget runs when a lane runs it (the command is in ENTRIES.md, "Open-and-idle cost"), and nothing in .github/ changes.

Overlap with #35270

#35270 (a lane whose session has ended) also adds gallery/app.html: one window over the app.window scenes in React, with model and browser tests. This PR covers more ground: any agent-pane fixture, the full axis table, scripts, CPU contention, the extension slot, the per-entry index that speeds up the whole gallery, the perf harness, and the chrome=native mode the view shell uses. The two touch the same paths, so only one should land. My take is to land this one and port #35270's browser test onto it; a coordinator decides.

Performance

Fleet Chromium, headless, CPU 1x, 2 s settle, then a 5 s idle window. "Before" is the base (4ce339304e67), measured with the same harness. The budgets are a first frame under 500 ms, no long task over 50 ms after ready, about 0% idle CPU and CLS 0.

ScenarioBefore: first frameAfter: first frameBefore: idle CPUAfter: idle CPUBefore: long task max after readyAfterBefore: CLSAfter
Mock app, whole window (new)n/a73 msn/a0.1%n/a0n/a0
Mock app, two panes, compact, light (new)n/a93 msn/a0.1%n/a0n/a0.0078
Mock app, three window sizes (new)n/a77 msn/a0.1%n/a0n/a0.0001
Mock app, script=streaming (animating on purpose) (new)n/a69 msn/a6.8%n/a0n/a0.0001
Gallery landing (one entry)366 ms274 ms0.1%0.1%125 ms109 ms00
app.window scene113 ms106 ms0.1%0.1%197 ms242 ms0.010.0102
app.window, every variant86 ms110 ms5.7%3.3%160 ms164 ms0.0160.0151
Whole gallery (browse, 24 live previews)2132 ms318 ms12.8%9.1%120 ms126 ms0.00010.4966

The mock app meets every budget at 1x: first frame under 100 ms, no long task after ready, 0 rAF and 0 timers while idle. The streaming row is expected to use CPU, because it animates.

Still over budget, all in the older gallery views, for a follow-up:

  • Browse idle CPU. This comes from the harness's own loops: the shift.ts box tracker rAF, the play runner's waitFor rAF poll, and the markReadyWhenStill timer in frame/main.ts.
  • Browse CLS. It rose because the previews now paint sooner and then settle.
  • The hidden "next" iframe preload in Stage.tsx.

At CPU 4x on the base, the browse view used 67.8% idle CPU with a 626 ms longest task, and the variants view used 93.7% idle CPU. I have not run the 4x pass on this branch yet.

Testing

  • bun run check in webviews/ passed on the fleet (fast checks) at this head, after vp fmt.
  • I ran the perf harness on the fleet at the base and at this head (the table above).
  • I have not run it in a real WKWebView; a capture mini does that in the native shell PR.

Observed / Not run

  • Observed: the harness numbers above; the mock app reaches ready in all four scenarios; and vp fmt, lint and typecheck are clean.
  • Not run: the 4x pass at head, and a manual pass in desktop Chrome on a person's machine.

Changelog

none

๐Ÿค– Generated with Claude Code

Agent session: agent session


Autofix with [code]smith
Open in [code]smith ยท Need help on this PR? Tag @codesmith-bot with what you need.


Summary by cubic

Adds the gallery's mock app at mock-app.html: cmux NEXT's whole window โ€” sidebar, tab strips, traffic lights, and each pane's real stage surface โ€” shown on one URL. Every condition is a URL query key in mock-app/axes.ts: fixtures and layouts, window sizes, theme and density, dynamic chat scripts, CPU contention, and an extension slot. Scripts, CPU contention, and extension slots run on the main pane only, since the panes share one main thread. Stage frames now load only their own entry through a new virtual entry-index module, which keys each file by its exported entry's id; keying by the first id literal found mis-mapped 108 of 428 files (nested picker or scenario ids sit above the entry's own), making the whole registry load.

The mock app moved from app.html so it sits beside #35270's page of the same name, which landed; the view shell, dev entries, and static build input follow the new path. The dev server's page matcher now routes /gallery/mock-app.html so it no longer falls through to the default page, and a route test covers it serving beside app.html. No changelog.

Performance

  • At 1x CPU the mock app meets every budget: first frame under 100 ms, no long task after ready, ~0% idle CPU, no layout shift; the script=streaming row animates on purpose and is the expected exception.
  • The per-entry loading cut the whole-gallery browse view's first frame from 2132 ms to 318 ms.
  • Browse idle CPU and CLS remain over budget because of the older views' own loops (CLS rose since previews now paint sooner), left for a follow-up.
  • scripts/gallery-matrix/app-perf.ts measures each scenario against budgets in app-perf-budgets.json and prints warnings on misses; it is lane-run, not a CI gate (CLAUDE.md).