feat(page-editor): Page Editor agent with design systems, live preview, and zero-build export#3370
feat(page-editor): Page Editor agent with design systems, live preview, and zero-build export#3370vibegui wants to merge 1 commit into
Conversation
🧪 BenchmarkShould we run the Virtual MCP strategy benchmark for this PR? React with 👍 to run the benchmark.
Benchmark will run on the next push after you react. |
Release OptionsSuggested: Minor ( React with an emoji to override the release type:
Current version:
|
There was a problem hiding this comment.
8 issues found across 31 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/mesh/src/api/routes/proxy.ts">
<violation number="1" location="apps/mesh/src/api/routes/proxy.ts:98">
P2: Dev-assets support was added for `/:connectionId` but not for `/:connectionId/call-tool/:toolName`, so direct call-tool requests against `{org}_dev-assets` still return 404.</violation>
</file>
<file name="apps/mesh/src/web/components/home/page-editor-recruit-modal.tsx">
<violation number="1" location="apps/mesh/src/web/components/home/page-editor-recruit-modal.tsx:325">
P1: Updating an existing Page Editor agent drops required page-editor tools from `selected_tools`, so the updated agent can no longer execute its own build workflow.</violation>
</file>
<file name="apps/mesh/src/web/views/virtual-mcp/index.tsx">
<violation number="1" location="apps/mesh/src/web/views/virtual-mcp/index.tsx:839">
P2: `Page preview` becomes a one-way default: after switching away once, this option disappears and can’t be re-selected.</violation>
</file>
<file name="apps/mesh/src/api/routes/page-preview.ts">
<violation number="1" location="apps/mesh/src/api/routes/page-preview.ts:127">
P2: Do not return raw internal error messages from `/export`; this can leak server filesystem details. Return a sanitized message instead.</violation>
</file>
<file name="apps/mesh/src/mcp-clients/client.ts">
<violation number="1" location="apps/mesh/src/mcp-clients/client.ts:53">
P1: The new SELF detection is too broad: `endsWith("_self")` can misroute non-SELF user connections to the in-process management MCP.</violation>
</file>
<file name="apps/mesh/src/page-preview/service.ts">
<violation number="1" location="apps/mesh/src/page-preview/service.ts:1051">
P1: Escape `</script>` before embedding inline module code in exported HTML to prevent script-breakout injection.</violation>
</file>
<file name="apps/mesh/src/web/layouts/main-panel-tabs/page-preview-tab.tsx">
<violation number="1" location="apps/mesh/src/web/layouts/main-panel-tabs/page-preview-tab.tsx:131">
P2: Use an exact slug/path-segment check instead of substring matching when deciding whether PAGE_PREVIEW_SET activated the current session page.</violation>
<violation number="2" location="apps/mesh/src/web/layouts/main-panel-tabs/page-preview-tab.tsx:715">
P1: The iframe sandbox currently allows both scripts and same-origin, which effectively defeats sandbox isolation for same-origin generated preview content.</violation>
</file>
Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.
| if (connection.id.endsWith("_self")) { | ||
| return connectInProcess(await managementMCP(ctx), "self-in-process"); | ||
| } |
There was a problem hiding this comment.
P1: The new SELF detection is too broad: endsWith("_self") can misroute non-SELF user connections to the in-process management MCP.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/mcp-clients/client.ts, line 53:
<comment>The new SELF detection is too broad: `endsWith("_self")` can misroute non-SELF user connections to the in-process management MCP.</comment>
<file context>
@@ -28,6 +50,9 @@ export async function clientFromConnection(
ctx: MeshContext,
superUser = false,
): Promise<Client> {
+ if (connection.id.endsWith("_self")) {
+ return connectInProcess(await managementMCP(ctx), "self-in-process");
+ }
</file context>
| if (connection.id.endsWith("_self")) { | |
| return connectInProcess(await managementMCP(ctx), "self-in-process"); | |
| } | |
| const selfId = `${connection.organization_id}_self`; | |
| if (connection.id === selfId) { | |
| return connectInProcess(await managementMCP(ctx), "self-in-process"); | |
| } |
| `<style>\n${tokensCss}\n</style>`, | ||
| ); | ||
| html = html.replace( | ||
| /<script[^>]*?src=["']\.\/app\.js["'][^>]*?>\s*<\/script>/g, |
There was a problem hiding this comment.
P1: Escape </script> before embedding inline module code in exported HTML to prevent script-breakout injection.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/page-preview/service.ts, line 1051:
<comment>Escape `</script>` before embedding inline module code in exported HTML to prevent script-breakout injection.</comment>
<file context>
@@ -0,0 +1,1165 @@
+ `<style>\n${tokensCss}\n</style>`,
+ );
+ html = html.replace(
+ /<script[^>]*?src=["']\.\/app\.js["'][^>]*?>\s*<\/script>/g,
+ `<script type="module">\n${inlineModule}\n</script>`,
+ );
</file context>
| title="Page preview" | ||
| src={liveUrl} | ||
| className="absolute inset-0 w-full h-full border-0 bg-white" | ||
| sandbox="allow-scripts allow-same-origin allow-forms allow-popups" |
There was a problem hiding this comment.
P1: The iframe sandbox currently allows both scripts and same-origin, which effectively defeats sandbox isolation for same-origin generated preview content.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/web/layouts/main-panel-tabs/page-preview-tab.tsx, line 715:
<comment>The iframe sandbox currently allows both scripts and same-origin, which effectively defeats sandbox isolation for same-origin generated preview content.</comment>
<file context>
@@ -0,0 +1,853 @@
+ title="Page preview"
+ src={liveUrl}
+ className="absolute inset-0 w-full h-full border-0 bg-white"
+ sandbox="allow-scripts allow-same-origin allow-forms allow-popups"
+ />
+ )}
</file context>
| sandbox="allow-scripts allow-same-origin allow-forms allow-popups" | |
| sandbox="allow-scripts allow-forms allow-popups" |
| // unscoped /mcp/{connectionId}_dev-assets route registered in dev-only.ts | ||
| // so frontend code using the canonical /api/:org/mcp/<id> URL still | ||
| // reaches the dev-assets MCP server in dev mode. | ||
| if (connectionId.endsWith("_dev-assets")) { |
There was a problem hiding this comment.
P2: Dev-assets support was added for /:connectionId but not for /:connectionId/call-tool/:toolName, so direct call-tool requests against {org}_dev-assets still return 404.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/api/routes/proxy.ts, line 98:
<comment>Dev-assets support was added for `/:connectionId` but not for `/:connectionId/call-tool/:toolName`, so direct call-tool requests against `{org}_dev-assets` still return 404.</comment>
<file context>
@@ -88,6 +90,27 @@ export const createProxyRoutes = () => {
+ // unscoped /mcp/{connectionId}_dev-assets route registered in dev-only.ts
+ // so frontend code using the canonical /api/:org/mcp/<id> URL still
+ // reaches the dev-assets MCP server in dev mode.
+ if (connectionId.endsWith("_dev-assets")) {
+ const devOrgId = connectionId.slice(0, -"_dev-assets".length);
+ if (!ctx.organization || ctx.organization.id !== devOrgId) {
</file context>
| ? await buildPageExportBundle({ orgId: org.id, slug }) | ||
| : await buildDesignSystemExportBundle({ orgId: org.id, slug }); | ||
| } catch (err) { | ||
| throw new HTTPException(404, { message: (err as Error).message }); |
There was a problem hiding this comment.
P2: Do not return raw internal error messages from /export; this can leak server filesystem details. Return a sanitized message instead.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/api/routes/page-preview.ts, line 127:
<comment>Do not return raw internal error messages from `/export`; this can leak server filesystem details. Return a sanitized message instead.</comment>
<file context>
@@ -0,0 +1,153 @@
+ ? await buildPageExportBundle({ orgId: org.id, slug })
+ : await buildDesignSystemExportBundle({ orgId: org.id, slug });
+ } catch (err) {
+ throw new HTTPException(404, { message: (err as Error).message });
+ }
+ const { bundleName, files } = bundle;
</file context>
There was a problem hiding this comment.
3 issues found across 4 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/mesh/src/page-preview/host-html.ts">
<violation number="1" location="apps/mesh/src/page-preview/host-html.ts:684">
P2: Incremental refresh re-renders without remounting, so section error boundaries can stay stuck in error state after a fix.</violation>
</file>
<file name="apps/mesh/src/web/layouts/main-panel-tabs/page-preview-tab.tsx">
<violation number="1" location="apps/mesh/src/web/layouts/main-panel-tabs/page-preview-tab.tsx:668">
P2: The design-system sync effect only watches `designSystems.length`, so metadata changes (name/brand edits) are missed and the host grid can display stale data.</violation>
<violation number="2" location="apps/mesh/src/web/layouts/main-panel-tabs/page-preview-tab.tsx:867">
P2: Re-keying the iframe by `refreshNonce` can break the host handshake lifecycle because readiness is not reset per iframe instance, so init intent messages may not be replayed to the new iframe.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
| ); | ||
| // Send filesBase once we're ready so dynamic-import URLs resolve. | ||
| win.postMessage({ type: "host:hello", filesBase }, "*"); | ||
| }, [hostReady, designSystems.length, filesBase]); |
There was a problem hiding this comment.
P2: The design-system sync effect only watches designSystems.length, so metadata changes (name/brand edits) are missed and the host grid can display stale data.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/web/layouts/main-panel-tabs/page-preview-tab.tsx, line 668:
<comment>The design-system sync effect only watches `designSystems.length`, so metadata changes (name/brand edits) are missed and the host grid can display stale data.</comment>
<file context>
@@ -496,28 +556,125 @@ export function PagePreviewTab() {
+ );
+ // Send filesBase once we're ready so dynamic-import URLs resolve.
+ win.postMessage({ type: "host:hello", filesBase }, "*");
+ }, [hostReady, designSystems.length, filesBase]);
const handleExport = () => {
</file context>
There was a problem hiding this comment.
1 issue found across 2 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/mesh/src/page-preview/host-html.ts">
<violation number="1" location="apps/mesh/src/page-preview/host-html.ts:381">
P2: Do not swallow all `tokens.js` import errors; only ignore true “missing module” cases and rethrow other failures so broken design-system code is visible.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
| const tokensMod = await import(state.filesBase + '/files/design-systems/' + encodeURIComponent(dsSlug) + '/tokens.js?v=' + v); | ||
| brand = tokensMod.BRAND; | ||
| } catch (err) { | ||
| console.warn('[host] design system "' + dsSlug + '" not found — using current brand', err); |
There was a problem hiding this comment.
P2: Do not swallow all tokens.js import errors; only ignore true “missing module” cases and rethrow other failures so broken design-system code is visible.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/page-preview/host-html.ts, line 381:
<comment>Do not swallow all `tokens.js` import errors; only ignore true “missing module” cases and rethrow other failures so broken design-system code is visible.</comment>
<file context>
@@ -364,12 +364,23 @@ export const PAGE_PREVIEW_HOST_HTML = `<!doctype html>
+ const tokensMod = await import(state.filesBase + '/files/design-systems/' + encodeURIComponent(dsSlug) + '/tokens.js?v=' + v);
+ brand = tokensMod.BRAND;
+ } catch (err) {
+ console.warn('[host] design system "' + dsSlug + '" not found — using current brand', err);
+ }
+ return { brand, Sections: sectionsMod, blocks: pageMod.PAGE || [] };
</file context>
Tip: Review your code locally with the cubic CLI to iterate faster.
There was a problem hiding this comment.
1 issue found across 2 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/mesh/src/api/routes/proxy.ts">
<violation number="1" location="apps/mesh/src/api/routes/proxy.ts:98">
P2: Dev-assets support was added for `/:connectionId` but not for `/:connectionId/call-tool/:toolName`, so direct call-tool requests against `{org}_dev-assets` still return 404.</violation>
</file>
<file name="apps/mesh/src/api/routes/page-preview.ts">
<violation number="1" location="apps/mesh/src/api/routes/page-preview.ts:127">
P2: Do not return raw internal error messages from `/export`; this can leak server filesystem details. Return a sanitized message instead.</violation>
</file>
<file name="apps/mesh/src/mcp-clients/client.ts">
<violation number="1" location="apps/mesh/src/mcp-clients/client.ts:53">
P1: The new SELF detection is too broad: `endsWith("_self")` can misroute non-SELF user connections to the in-process management MCP.</violation>
</file>
<file name="apps/mesh/src/page-preview/service.ts">
<violation number="1" location="apps/mesh/src/page-preview/service.ts:1051">
P1: Escape `</script>` before embedding inline module code in exported HTML to prevent script-breakout injection.</violation>
</file>
<file name="apps/mesh/src/web/layouts/main-panel-tabs/page-preview-tab.tsx">
<violation number="1" location="apps/mesh/src/web/layouts/main-panel-tabs/page-preview-tab.tsx:668">
P2: The design-system sync effect only watches `designSystems.length`, so metadata changes (name/brand edits) are missed and the host grid can display stale data.</violation>
<violation number="2" location="apps/mesh/src/web/layouts/main-panel-tabs/page-preview-tab.tsx:715">
P1: The iframe sandbox currently allows both scripts and same-origin, which effectively defeats sandbox isolation for same-origin generated preview content.</violation>
</file>
<file name="apps/mesh/src/page-preview/host-html.ts">
<violation number="1" location="apps/mesh/src/page-preview/host-html.ts:381">
P2: Do not swallow all `tokens.js` import errors; only ignore true “missing module” cases and rethrow other failures so broken design-system code is visible.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
6593dfb to
23c3395
Compare
|
You're iterating quickly on this pull request. To help protect your rate limits, cubic has paused automatic reviews on new pushes for now—when you're ready for another review, comment |
98826df to
1a388f4
Compare
…s, choreographed build
Adds the Page Editor: a new builtin agent that builds landing pages
section-by-section in front of the user via a real-time preview pane.
The agent emits PAGE_* tool calls (PAGE_BOOTSTRAP, PAGE_RENDER_BLOCK,
PAGE_UPDATE_BLOCK, PAGE_REMOVE_BLOCK, PAGE_REVIEW_SUGGEST, and DS
management) which Studio observes from the chat stream and dispatches
straight into an iframe as host:* postMessages — a browser-as-REPL
pipeline that skips HTTP round-trips per block for ~10× faster builds.
Main pieces:
- apps/mesh/src/tools/page-preview/ — MCP tool surface
- apps/mesh/src/page-preview/service.ts — server-side persistence
- apps/mesh/src/page-preview/templates.ts — ~24 section library
- apps/mesh/src/page-preview/host-html.ts — self-contained Preact
runtime served into the
preview iframe
- apps/mesh/src/web/layouts/main-panel-tabs/page-preview-tab.tsx
— Studio-side host + bridge
- apps/mesh/src/web/components/home/page-editor-recruit-modal.tsx
— seven-question welcome quiz
Build choreography (in host-html.ts):
- Phases: prelude → design → layout → building → done
- UnifiedDesignPhase: split-screen DS gallery + section library with
the agent's outline highlighted (staggered pill animations)
- OutlineStepper: sticky stepper with click-to-time-travel preview
- queueReveal: paced reveal queue (MIN_REVEAL_INTERVAL_MS = 1500)
so each new section gets reading room before the page scrolls
- Refresh of a done page short-circuits all choreography
Also adds:
- Contrast math (onPrimary/onSecondary/onAccent tokens) for readable
buttons on brand backgrounds
- Auto-bubble of preview runtime errors back to the agent
- GEO/SEO baseline (JSON-LD, llms.txt, robots.txt)
- WELL_KNOWN_AGENT_TEMPLATES entry + home-screen tile
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
0260fec to
20a1187
Compare
Summary
Adds a local-first Page Editor agent that builds zero-build landing pages with Claude Code, plus a dedicated preview pane and a scaffolding pipeline that splits pages from design systems.
design-systems/<slug>/(tokens.css, tokens.js, demo.html, meta.json) andpages/<slug>/(index.html, app.js, sections.js, page.js, meta.json). Pages bind to a design system via meta.json.DESIGN_SYSTEM_CREATE / LIST / SET,PAGE_PREVIEW_PAGE_CREATE, alongside the existingPAGE_PREVIEW_STATUS / SET / REFRESH. Scaffolding is template-driven so the agent doesn't hand-roll boilerplate — stages 1 and 2 are single tool calls that switch the preview within ~50ms.index.htmlinlines the bound design system's CSS as<style>and consolidates the local JS modules into one inline<script type="module">, so unzip-and-double-click works. Original multi-file source preserved undersrc/.DESIGN_SYSTEM_CREATE:fg≥ 7:1,muted≥ 5.5:1,border≥ 1.5:1 againstbg, with mixing towardfgto preserve hue. Fixes the recurring pastel-on-pastel illegibility from agent-generated palettes.mcp-clients/client.ts: SELF (<orgId>_self) pseudo-connections route to an in-process MCP server overInMemoryTransportinstead of an HTTP self-roundtrip. The HTTP path failed in conductor worktrees because Bun fetch on macOS can't resolve arbitrary*.localhostsubdomains, so the virtual MCP's tool list never reached Claude Code.lazy-client.ts: cache bypass for in-process MCP servers so newly-added management tools show up immediately.templates.ts:tokens.jsrendered viaJSON.stringifyso font stacks with embedded quotes can't produce SyntaxErrors;tokens.cssfont interpolation normalized via a small helper.ErrorBoundary— a single broken section now shows a small inline error instead of blanking the page.Test plan
bun run check— cleanbun test apps/mesh/src/page-preview/— 24 pass / 0 fail (77 assertions)apps/mesh/scripts/test-page-preview-mcp.tsdrives the live virtual-MCP endpoint end-to-end:initialize→tools/list→DESIGN_SYSTEM_CREATE→PAGE_PREVIEW_PAGE_CREATE→PAGE_PREVIEW_REFRESH→GET /export(validates zip magic bytes) →PAGE_PREVIEW_STATUS. PASS on a fresh server.muted: "#E5DDF3"onbg: "#F3EBFF") and confirm the on-disktokens.cssends up legible.🤖 Generated with Claude Code
Demo readiness fixes (2026-05-15)
Latest commit (
aa7622ecd) ships a tight cluster of end-to-end fixes shaking out during demo prep:PAGE_PREVIEW_PAGE_CREATE,PAGE_PREVIEW_PROGRESS, and the threeDESIGN_SYSTEM_*tools from existing agents'selected_tools. Re-recruiting an existing Page Editor stripped its mandated first-call tools. Unified both branches behind a singlePAGE_EDITOR_SELECTED_TOOLSconstant.PAGE_PREVIEW_PAGE_CREATE: the agent reliably emitted a long prose plan and ended its turn without promoting the preview. Added anextStepadvisory to each chain-driving tool's response (DESIGN_SYSTEM_CREATE,PAGE_PREVIEW_PAGE_CREATE,PAGE_PREVIEW_SET,PAGE_PREVIEW_REFRESH) naming the exact next 1–3 tool calls and the live slug. Tool-response nudges are far stickier than top-of-prompt rules for stopping prose-planning regressions. System prompt also gets a new "THE ONE RULE" section.nextStepwas citing made-up prop names. Auditedtemplates.ts:662–873and rewrotenextStep+ system prompt with the actual contracts for all ten library sections.DESIGN_SYSTEM_CREATE's description claimed missing fields get "sensible defaults" — butdefaultBrand()is dark-neon indigo on near-black, not a smart default for arbitrary briefs. The agent would call DS_CREATE sparse, see wrong colors, then re-call with the real palette. Tightened description + prompt to commit the full palette on the first call.state.isRunningso it fades out the moment the agent's turn ends.activePage/showKindserver-state fallbacks were firing whenever this chat had no session DS/page yet — including for brand-new chats where the agent had only calledPAGE_PREVIEW_PROGRESS.state.jsonfrom a previous chat would pull the old page into the preview. Both fallbacks now also gate on!previewToolFiredEarlyso they only fire for true cold loads.Summary by cubic
Adds a local‑first Page Editor agent with design systems, a stable host‑iframe live preview, a time‑travel stepper, and zero‑build export. Also adds a Page Preview tab and a faster browser‑as‑REPL build loop with paced section reveals.
New Features
PAGE_*andDESIGN_SYSTEM_*tools and org‑scoped/api/:org/page-preview/*routes with an in‑iframe Preact host (postMessage bridge, CSPSAMEORIGIN). Includes a Page Preview system tab and a recruit quiz.fflate, WCAG contrast enforcement (incl.onPrimary/onSecondary/onAccent), and curated default themes.Bug Fixes
Written for commit 20a1187. Summary will update on new commits. Review in cubic