Skip to content

feat(page-editor): Page Editor agent with design systems, live preview, and zero-build export#3370

Open
vibegui wants to merge 10 commits into
mainfrom
vibegui/page-editor-agent
Open

feat(page-editor): Page Editor agent with design systems, live preview, and zero-build export#3370
vibegui wants to merge 10 commits into
mainfrom
vibegui/page-editor-agent

Conversation

@vibegui

@vibegui vibegui commented May 15, 2026

Copy link
Copy Markdown
Contributor

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.

  • Two-concept storage: design-systems/<slug>/ (tokens.css, tokens.js, demo.html, meta.json) and pages/<slug>/ (index.html, app.js, sections.js, page.js, meta.json). Pages bind to a design system via meta.json.
  • New MCP tools: DESIGN_SYSTEM_CREATE / LIST / SET, PAGE_PREVIEW_PAGE_CREATE, alongside the existing PAGE_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.
  • Preview pane: dual selector (page + design system), a welcome quiz on every fresh chat that composes a prompt and drops it into the chat input, an Export button, and an iframe that re-keys on file changes so the latest bytes always render.
  • Self-contained zip export: index.html inlines 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 under src/.
  • WCAG-based contrast enforcement runs on every DESIGN_SYSTEM_CREATE: fg ≥ 7:1, muted ≥ 5.5:1, border ≥ 1.5:1 against bg, with mixing toward fg to preserve hue. Fixes the recurring pastel-on-pastel illegibility from agent-generated palettes.
  • Robustness fixes hit along the way:
    • mcp-clients/client.ts: SELF (<orgId>_self) pseudo-connections route to an in-process MCP server over InMemoryTransport instead of an HTTP self-roundtrip. The HTTP path failed in conductor worktrees because Bun fetch on macOS can't resolve arbitrary *.localhost subdomains, 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.js rendered via JSON.stringify so font stacks with embedded quotes can't produce SyntaxErrors; tokens.css font interpolation normalized via a small helper.
    • app.js template wraps each section in a Preact ErrorBoundary — a single broken section now shows a small inline error instead of blanking the page.

Test plan

  • bun run check — clean
  • bun test apps/mesh/src/page-preview/ — 24 pass / 0 fail (77 assertions)
  • Closed-loop integration script apps/mesh/scripts/test-page-preview-mcp.ts drives the live virtual-MCP endpoint end-to-end: initializetools/listDESIGN_SYSTEM_CREATEPAGE_PREVIEW_PAGE_CREATEPAGE_PREVIEW_REFRESHGET /export (validates zip magic bytes) → PAGE_PREVIEW_STATUS. PASS on a fresh server.
  • Manual: recruit Page Editor → quiz welcome → submit prompt → preview shows design-system demo → page shell appears → section edits trigger staggered fade-in reveal → export download produces a double-click-openable bundle.
  • Reviewer: try a deliberately bad palette (e.g. muted: "#E5DDF3" on bg: "#F3EBFF") and confirm the on-disk tokens.css ends 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:

  • Recruit modal: update-path was dropping PAGE_PREVIEW_PAGE_CREATE, PAGE_PREVIEW_PROGRESS, and the three DESIGN_SYSTEM_* tools from existing agents' selected_tools. Re-recruiting an existing Page Editor stripped its mandated first-call tools. Unified both branches behind a single PAGE_EDITOR_SELECTED_TOOLS constant.
  • Agent stalls after PAGE_PREVIEW_PAGE_CREATE: the agent reliably emitted a long prose plan and ended its turn without promoting the preview. Added a nextStep advisory 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.
  • Hero (and other sections) rendered template defaults ("Build a beautiful page.") because the nextStep was citing made-up prop names. Audited templates.ts:662–873 and rewrote nextStep + system prompt with the actual contracts for all ten library sections.
  • Wrong design-system colors flashed on screen: DESIGN_SYSTEM_CREATE's description claimed missing fields get "sensible defaults" — but defaultBrand() 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.
  • Outline stepper persisted past the build: gated on state.isRunning so it fades out the moment the agent's turn ends.
  • Session isolation: the activePage / showKind server-state fallbacks were firing whenever this chat had no session DS/page yet — including for brand-new chats where the agent had only called PAGE_PREVIEW_PROGRESS. state.json from a previous chat would pull the old page into the preview. Both fallbacks now also gate on !previewToolFiredEarly so they only fire for true cold loads.

Summary by cubic

Ships a local‑first Page Editor with design systems, live preview, time‑travel, and zero‑build export — auto‑installed as a Studio Pack default that opens in the new Page Preview tab. Adds a self‑verifying feature catalog and CLI for the Page Editor, now with a browser e2e leg and Webwright tasks.

  • New Features

    • Studio Pack integration: Page Editor auto‑installs for all orgs and defaults to the "Page Preview" main view.
    • Browser‑as‑REPL preview: PAGE_BOOTSTRAP, PAGE_RENDER_BLOCK, PAGE_UPDATE_BLOCK, PAGE_REMOVE_BLOCK, PAGE_REVIEW_SUGGEST stream to an in‑iframe Preact host via postMessage for ~10× faster builds. Includes a split‑screen design‑system gallery, 24‑section library, time‑travel, and a paced reveal queue (honors reduced‑motion).
    • Preview pipeline: org‑scoped /api/:org/page-preview/* routes, including a framable host shell (X‑Frame‑Options SAMEORIGIN), a dev‑mode auto‑reload hook via /api/:org/page-preview/host-version, and export endpoints.
    • Zero‑build export and readability: one‑file zip with inlined CSS/JS via fflate, curated default themes, and WCAG contrast enforcement with derived onPrimary/onSecondary/onAccent. Adds JSON‑LD + llms.txt + robots.txt for crawlers.
    • Feature catalog: new features/ harness with feature.md + happy-path.test.ts and CLI (features:list|test|new); Page Editor registered with a full happy‑path test. Adds a Playwright browser leg (PW=1 runs both legs) that verifies the Page Preview iframe boots, plus Webwright tasks and AGENTS.md guidance.
    • Preview resilience: runtime errors from the iframe bubble back to the agent for faster fixes.
  • Bug Fixes

    • Multi‑tenant isolation: migrated page/design‑system persistence to org‑bound object storage (served via /api/:org/files/page-preview/*), fixing cross‑org bleed and enabling multi‑instance deploys. Also prunes page-preview/* objects when the Page Editor vMCP is deleted.
    • Concurrency and stability: per‑org state lock wraps all state reads/writes; finished pages refresh read‑only; in‑process SELF/dev‑assets MCP replaces flaky HTTP (with a dedicated pseudo‑connection route); caches bypassed for in‑process servers; filesystem/shell/search tools hard‑disabled for this agent at the SDK level.
    • Safety hardening: sanitize href/src props to allow only http(s)/mailto/tel/relative/hash; reject empty or whitespace slugs with a strict SlugSchema.
    • Stream/UX polish: dedup stream replays on task changes; outline stepper hides when the run ends; unified stream walker via a single walkParts iterator; iframe postMessage bridge extracted to a hook and gains dev auto‑reload; chat input bridge enables prompt composition from the preview. Unit tests cover contrast/service/tool handlers via the feature harness.

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

Review in cubic

@github-actions

Copy link
Copy Markdown
Contributor

🧪 Benchmark

Should we run the Virtual MCP strategy benchmark for this PR?

React with 👍 to run the benchmark.

Reaction Action
👍 Run quick benchmark (10 & 128 tools)

Benchmark will run on the next push after you react.

@github-actions

github-actions Bot commented May 15, 2026

Copy link
Copy Markdown
Contributor

Release Options

Suggested: Minor (2.368.0) — based on feat: prefix

React with an emoji to override the release type:

Reaction Type Next Version
👍 Prerelease 2.367.8-alpha.1
🎉 Patch 2.367.8
❤️ Minor 2.368.0
🚀 Major 3.0.0

Current version: 2.367.7

Note: If multiple reactions exist, the smallest bump wins. If no reactions, the suggested bump is used (default: patch).

@cubic-dev-ai cubic-dev-ai Bot left a comment

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.

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.

Comment thread apps/mesh/src/web/components/home/page-editor-recruit-modal.tsx Outdated
Comment on lines +53 to +55
if (connection.id.endsWith("_self")) {
return connectInProcess(await managementMCP(ctx), "self-in-process");
}

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: 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>
Suggested change
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,

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: 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"

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: 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>
Suggested change
sandbox="allow-scripts allow-same-origin allow-forms allow-popups"
sandbox="allow-scripts allow-forms allow-popups"

Comment thread apps/mesh/src/api/routes/proxy.ts
Comment thread apps/mesh/src/web/views/virtual-mcp/index.tsx
? await buildPageExportBundle({ orgId: org.id, slug })
: await buildDesignSystemExportBundle({ orgId: org.id, slug });
} catch (err) {
throw new HTTPException(404, { message: (err as Error).message });

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: 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>

Comment thread apps/mesh/src/web/layouts/main-panel-tabs/page-preview-tab.tsx Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

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.

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.

Comment thread apps/mesh/src/page-preview/host-html.ts
);
// Send filesBase once we're ready so dynamic-import URLs resolve.
win.postMessage({ type: "host:hello", filesBase }, "*");
}, [hostReady, designSystems.length, filesBase]);

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: 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>

Comment thread apps/mesh/src/web/layouts/main-panel-tabs/page-preview-tab.tsx Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

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.

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);

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: 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.

@cubic-dev-ai cubic-dev-ai Bot left a comment

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.

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/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

Comment thread apps/mesh/src/page-preview/host-html.ts Outdated
@vibegui vibegui force-pushed the vibegui/page-editor-agent branch from 6593dfb to 23c3395 Compare May 15, 2026 15:39
@cubic-dev-ai

cubic-dev-ai Bot commented May 15, 2026

Copy link
Copy Markdown
Contributor

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 @cubic-dev-ai review.

@vibegui vibegui force-pushed the vibegui/page-editor-agent branch 2 times, most recently from 98826df to 1a388f4 Compare May 20, 2026 13:09
@vibegui vibegui force-pushed the vibegui/page-editor-agent branch 5 times, most recently from b815e71 to d86c6e0 Compare May 28, 2026 18:01
vibegui and others added 10 commits May 28, 2026 15:25
…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>
Auto-installs for every org via the existing Studio Pack pipeline,
so users get the Page Editor out of the box (no recruit modal, no
home-tile wiring required).

- apps/mesh/src/tools/virtual/studio-pack/page-editor.ts: define
  pageEditorAgent with id, icon, instructions, selected tools
  (PAGE_*/DESIGN_SYSTEM_*), and defaultMainView=page-preview
- studio-pack/index.ts: add pageEditorAgent to STUDIO_PACK_AGENTS
  and extend installStudioPack to honor an optional defaultMainView
  on agents — sets metadata.ui.layout.defaultMainView so the
  panel-tab resolver renders the live preview iframe
- mesh-sdk constants: StudioPackAgentId.PAGE_EDITOR helper and
  isStudioPackAgent() prefix check
- app.ts: minor biome reformat (no semantic change)

New orgs install via the Better Auth org.afterCreate hook.
Existing orgs pick it up on next backfillStudioPackForAllOrgs run.

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

Fixes multi-tenant isolation and unblocks multi-instance deployment.

BEFORE: service.ts persisted pages and design systems as files under
<dataDir>/page-editor/{pages,design-systems,state.json} on the local
filesystem. getPagePreviewPaths(options) accepted an orgId argument
but ignored it — every org wrote to the same shared directory. The
service.test.ts even documented this with a "uses one well-known
local pages directory for every org" guard. Org A could read,
activate, and serve org B's pages. The setup also broke multi-pod
deployments since /data wasn't shared across instances.

AFTER: persistence flows through ctx.objectStorage (BoundObjectStorage),
which is org-bound at construction (see object-storage/factory.ts).
The org isolation is enforced by the binding — there is no orgId in
the page-preview keys at all. Keys are:

  page-preview/state.json
  page-preview/pages/<slug>/{index.html, app.js, sections.js, page.js, meta.json}
  page-preview/design-systems/<slug>/{demo.html, demo.js, tokens.css, tokens.js, meta.json}

Files are served via Studio's canonical /api/{org}/files/{key}
redirect (api/routes/files.ts), which handles presigned URLs and
the dev-vs-prod split. The dedicated /api/{org}/page-preview/files/*
route is deleted — page-preview now reuses the same files plumbing
the rest of Studio uses.

Files changed:
- apps/mesh/src/page-preview/service.ts: replace all readFile/
  writeFile/readdir/mkdir/stat with objectStorage.{get,put,list,
  head,delete}. Drop getPagePreviewPaths, getServingRoots,
  resolvePagePreviewAsset, insideAnyRoot, assertInside,
  toRelativePath, uniquePaths, isHtmlPath, dataDirOf, readJsonSafe,
  writeJson, readUtf8 — they're either replaced by I/O primitives
  or were disk-only concerns. defaultBrand() function → DEFAULT_BRAND
  const. PagePreviewOptions adds objectStorage and drops dataDir.
  Drops the ~/deco/page-editor legacy serving root.
- apps/mesh/src/api/routes/page-preview.ts: delete GET /files/* (use
  the canonical /api/{org}/files/{key} route instead). /state and
  /export thread objectStorage through.
- apps/mesh/src/page-preview/host-html.ts: deriveFilesBaseFromLocation
  now points at /api/<org>/files/page-preview. URL pattern drops the
  redundant /files/ segment (filesBase + '/pages/...' instead of
  filesBase + '/files/pages/...').
- apps/mesh/src/web/layouts/main-panel-tabs/page-preview-tab.tsx:
  filesBase swapped to /api/{org}/files/page-preview.
- apps/mesh/src/tools/page-preview/index.ts: orgArgs() now returns
  ctx.objectStorage. Tools fail fast with a clear error if storage
  isn't provisioned. defaultBrand() → DEFAULT_BRAND.
- apps/mesh/src/page-preview/service.test.ts: rewritten against
  DevObjectStorage. Adds an explicit multi-tenant isolation test
  (org A's binding cannot see org B's pages).

Verified:
- bun test apps/mesh/src/page-preview/ → 26/26 pass
- bun test apps/mesh/src/tools/virtual/studio-pack.test.ts → 4/4 pass
- bun run --cwd=apps/mesh check → no new TS errors

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- B3 slug validation: introduce a SlugSchema (z.string().min(1) + at
  least one alphanumeric) used by every create/activate tool. Stops
  whitespace-only or empty slugs at the Zod boundary so the resulting
  validation error names the slug field rather than slugify()'s
  generic "Invalid slug".

- B4 prop XSS sanitization: add sanitizeBlockProps() that walks an
  agent-supplied prop tree and collapses any href/src/-Href/-Src
  value with an unsafe URL scheme (javascript:, data:, vbscript:,
  anything outside http(s)/mailto/tel/relative/hash) down to "#".
  Applied at appendBlock and updateBlock in service.ts so the stored
  page.js export is safe. Mirrored inline in host-html.ts (as
  sanitizeBlockProps) so chat-stream-driven host:render-block and
  host:update-block dispatches are also sanitized — these don't go
  through the server-side write path.

- B7 OTel tracing: verified defineTool already wraps every handler
  in ctx.tracer.startActiveSpan with success/error status. No
  per-handler spans needed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…2,B6,C3,C5,C6,C7)

Track B (production hardening):
- B1 cleanup on vMCP delete: COLLECTION_VIRTUAL_MCP_DELETE now prunes
  the page-preview/* prefix in object storage when the deleted vMCP
  is the org's Page Editor agent. Fire-and-forget so a storage hiccup
  can't block the user's delete intent.
- B2 concurrent-write protection: introduces withStateLock(orgId, ...)
  — a per-org in-process promise chain — and wraps every state
  read-modify-write in setPagePreviewActive / setActiveDesignSystem /
  refreshPagePreview / setPageProgress / createDesignSystem /
  createPage / bumpRefreshVersion. Eliminates the within-pod
  last-writer-wins race on state.json. Multi-pod deployments still
  rely on object-storage put-per-key atomicity (which holds).
- B6 tool handler tests: 4 new tests for appendBlock / updateBlock /
  removeBlock covering section-name validation, duplicate rejection,
  post-Footer rejection, URL sanitization at append time, and the
  update/remove round-trip on the live block list.

Track C (simplifications):
- C3 import types in page-preview-tab.tsx: drop the locally-declared
  PageEntry / DesignSystemEntry / PreviewKind / PagePreviewStatus
  duplicates; import from service.ts so any new field lands here
  automatically.
- C5 parseJsExport helper: collapse parseTokensJsBrand +
  parsePageJsBlocks into a single regex-driven parseJsExport<T>.
- C6 deriveOnColorTokens: replace the 3 identical pickReadableText
  calls in normalizeBrandContrast with one inline `pick` helper.
- C7 theme catalogue: page-editor.ts INSTRUCTIONS no longer
  hand-syncs the 10-row markdown theme table. Now built at module
  load time from DEFAULT_THEMES. KNOWN_SECTION_NAMES exported for
  any future consumer.

Also lands B5 stream-replay dedup hardening: dispatchedBlockPatches
in page-preview-tab.tsx now resets when taskId changes, so
positional-fallback ids ("#0", "#1") from a prior chat can't collide
with the new chat's positions on cross-chat navigation.

Verified:
- bun test apps/mesh/src/page-preview/ → 35/35 pass
- bun test apps/mesh/src/tools/virtual/studio-pack.test.ts → 4/4 pass
- bun run --cwd=apps/mesh check → no new TS errors

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…elper (C2)

The three tool-arg readers (extractToolArgString, extractToolArgArray,
extractToolArgAny) each duplicated the same priority list of arg
buckets the Claude Code streaming protocol can put arguments in:
record.input, record.args, record.params.arguments.

Extract that list into readToolArgBuckets() and rebuild the typed
readers as thin wrappers on top. extractToolArgArray now delegates
to extractToolArgAny + a coercion; extractToolArgString keeps its
dot-path traversal but reads buckets from the shared helper.

Net: -10 lines, one source of truth for the bucket priority order.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Internal cleanups for page-preview-tab.tsx:

- C1 collapse 6 stream walkers: introduces a single walkParts(messages)
  generator that yields { toolName, record, state } from every tool-call
  part across the chat stream. Replaces the same nested
  `for-of messages → for-of parts → partToolName → record/state lift`
  pattern that was repeated in deriveSessionItems, hasAnyPreviewToolFired,
  deriveLiveOutline, deriveReviewTips, deriveLiveProgress, and
  deriveBlockPatchCalls. One iteration shape, one place to change.

- C4 extract useIframePostMessageBridge: lifts the ~120-line iframe
  message listener effect out of PagePreviewTab into a co-located hook
  that takes typed callback props for each event (host-ready,
  prompt, prompt-and-send, select-ds, close-ds-grid, request-refresh,
  runtime-error). The hook is the sole owner of the
  addEventListener/removeEventListener pair — lifecycle visible in
  one place, component body focused on intent dispatch + render.

Dev experience:

- Enable iframe auto-reload (effective HMR for the preview): the
  /host route in dev mode injects a tiny ~5-line poller that hits a
  new /host-version endpoint every 1.5s. The endpoint returns a
  sha1 prefix of PAGE_PREVIEW_HOST_HTML — bun --hot reloads the
  host-html module on every save, the export changes, the hash
  changes, the iframe location.reload()s. State is recovered the
  same way it is on first mount: Studio's tab.tsx re-dispatches
  intent via host:hello / host:set-page after the iframe handshake.
  Production gets the raw HTML (no poller, no extra requests).

Verified:
- bun test apps/mesh/src/page-preview/ → 35/35 pass
- bun run --cwd=apps/mesh check → no new TS errors

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Every major product feature now gets a co-located harness:
  features/<name>/feature.md           # story, value, happy path, prompt for AI agents
  features/<name>/happy-path.test.ts   # executable contract

The CLI at scripts/features.ts (wired as features:list / features:test /
features:new) lists and runs the catalog. AGENTS.md teaches the contract:
before touching code in a documented area, run the harness, extend the
test for the new behavior FIRST (RED), implement until GREEN, only then
ask a human to verify. "Studio needs to make itself."

This PR ships the spine + the first concrete entry:

- features/README.md — catalog invariants
- features/page-editor/feature.md — the Page Editor story end-to-end,
  including the happy path, the file list of what implements it, the
  Maintenance loop, and a prompt for AI agents extending the feature.
- features/page-editor/happy-path.test.ts — 9 active tests across 5
  phases (Studio Pack contract, drive the agent's tool sequence, assert
  server state, multi-tenant isolation, export bundle). Phase F (browser
  via Playwright, gated by PW=1) is reserved as describe.skip for a
  follow-up.
- scripts/features.ts — thin CLI that locates features and forwards to
  `bun test`. Exits non-zero when a catalogued feature lacks a test.
- package.json — wire features:list / features:test / features:new.
- AGENTS.md — new "Working on a feature (the harness contract)" section
  immediately after Overview, plus a pointer in Testing Guidelines.
- .gitignore — add .agents/ so Conductor stops flagging it.
- apps/mesh/scripts/test-page-preview-mcp.ts — deleted; superseded by
  the harness (its assertions are now Phase E + a much larger contract).

Verified end-to-end:
- `bun run features:list` → lists `page-editor` with tagline + test-present indicator.
- `bun run features:test page-editor` → 9 active pass, 1 skip (Phase F). ~150 ms.
- Deliberately remove `defaultMainView` from pageEditorAgent → Phase A
  fails on the exact contract line; restore → green.
- `bun test apps/mesh/src/page-preview/` still 35/35 pass (no
  regression on existing unit suite).
- `bun run --cwd=apps/mesh check` no new TS errors.

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

Two changes that flesh out the feature catalog beyond the data-path
contract that shipped last commit.

Phase F (deterministic, in CI):
- New apps/mesh/e2e/tests/features/page-editor.browser.spec.ts uses
  Playwright + the existing apps/mesh/e2e/fixtures/auth.ts signup
  helper. After signup, the spec navigates to /<org>/agents, waits for
  the Page Editor card to appear (Studio Pack install is async via a
  DBOS workflow — generous timeout absorbs the race), clicks into the
  agent, asserts the URL carries `virtualmcpid=studio-page-editor_*`,
  and confirms the iframe titled "Page preview" loads with the preact
  bundle actually executing through to the welcome state.
- scripts/features.ts now runs both legs: the in-process Bun tests
  always run; when PW=1, a matching `*.browser.spec.ts` next to the
  e2e fixtures is also executed via playwright. The dev server
  auto-starts via the existing playwright.config webServer.
- features/page-editor/happy-path.test.ts loses the Phase F skip
  placeholder (the real spec is the source of truth now).

Webwright (exploratory, before the human pass):
- Each feature.md now has an "Exploratory verification" section
  describing the happy path as a task for Microsoft Research's
  Webwright (https://github.com/microsoft/Webwright) — an MIT-licensed
  browser-agent framework that turns a plain-English task into a
  re-runnable Playwright script + screenshots + a numbered-CP
  self-verification log. Install via `/plugin install
  webwright@webwright` as a Claude Code skill.
- features/README.md explains the two-layer model: deterministic spec
  is the gate (CI), Webwright is the canary (an LLM stress-tests the
  happy path against the live UI before a human verifies). Output is
  evidence for the human, never a substitute for the deterministic
  contract.
- AGENTS.md gains a brief pointer to Webwright in the "Working on a
  feature" section.

Verified:
- bun run features:test page-editor → 9/9 pass (~150 ms).
- bun run --cwd=apps/mesh check → no new TS errors.
- bun run fmt clean.

PW=1 path is exercised against a running dev server; CI nightly job
to actually invoke it is Phase 2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two pieces of churn from main:
- studio-pack/types.ts dropped BuildWelcomeMessage / WelcomeContext —
  the welcome message field is no longer part of the contract. Drop
  the import + the inline welcomeMessage definition; the agent gets
  its first impression from the chat composer's empty state, same as
  the other Studio Pack agents (Agent Manager, Connection Manager,
  Automation Manager).
- Every other Studio Pack agent now declares a `selectedPrompts`
  array; the install loop reads it as `agent.selectedPrompts`. Add
  the empty array so the TS union narrows correctly.

Verified:
- bun run --cwd=apps/mesh check → no TS errors in page-preview /
  studio-pack.
- bun test apps/mesh/src/page-preview/ + studio-pack.test.ts → 39/39
  pass.
- bun run features:test page-editor → 9/9 pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vibegui vibegui force-pushed the vibegui/page-editor-agent branch from d86c6e0 to a0d01e9 Compare May 28, 2026 18:25
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