Skip to content

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

Open
vibegui wants to merge 24 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 24 commits into
mainfrom
vibegui/page-editor-agent

Conversation

@vibegui
Copy link
Copy Markdown
Contributor

@vibegui vibegui commented May 15, 2026

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

Adds a local‑first Page Editor with a single host‑iframe live preview, step‑by‑step progress, an interactive time‑travel stepper, and zero‑build export. Pages and design systems are split; you can retheme or switch pages without reloads, and the welcome quiz is now a 7‑question optional flow that composes a grounded prompt. Preview runtime errors now auto‑report to the agent when idle for faster fixes.

  • New Features

    • Host iframe at /api/:org/page-preview/host driven by postMessage (welcome, set/refresh page, retheme, inline design‑system gallery/grid) with crossfades, a thinking intermission, section reveals + auto‑scroll, and a progress overlay + outline stepper via PAGE_PREVIEW_PROGRESS — the stepper is now clickable to time‑travel between steps, with a “Live ›” pill to return to live; a “Design system” step is prepended and future steps are inert.
    • Unified flow: DS demo renders inline in page mode; persistent Nav/Footer shell; DS dropdown rethemes the current page; pages start as export const PAGE = [] with a richer sections library; only new blocks animate in.
    • Scaffolding + tools: DESIGN_SYSTEM_CREATE/LIST/SET, PAGE_PREVIEW_PAGE_CREATE{activate?}, PAGE_PREVIEW_STATUS/SET/REFRESH, PAGE_PREVIEW_PROGRESS; next‑step hints guide the agent through build steps.
    • Export: self‑contained zip (via fflate) that inlines CSS, hoists a single preact/htm import, synthesizes a Sections namespace, and preserves sources under src/.
    • Welcome quiz: expanded to seven fully‑optional questions with optional details; composes a concise, verbatim‑grounded prompt (includes “Nothing yet — do not fabricate” for proof).
    • Error handling: the preview iframe auto‑bubbles runtime errors to the agent when the task is idle (deduped); falls back to composing the chat input if no stream is active, and still offers “Ask the agent to fix this.”
  • Bug Fixes

    • Stable host iframe and bridge: no remounts on dropdown changes; retheme without reload; resilient handshake; clears error card on success; SAMEORIGIN framing for /page-preview/files and /page-preview/host.
    • Session freshness/isolation: ignore non‑final tool outputs and non‑existent slugs; preview scoped to the current chat; SELF and dev‑assets pseudo‑connections handled in‑process; SWR bypass so new tools appear immediately.
    • Progress/outline polish: outline persists across DESIGN_SYSTEM_CREATE/PAGE_PREVIEW_PAGE_CREATE and updates during inline DS gallery; thinking intermission holds until real content; fallback “Working…” pill between labels.

Written for commit 98826df. 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
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 15, 2026

Release Options

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

React with an emoji to override the release type:

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

Current version: 2.329.0

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

Copy link
Copy Markdown
Contributor

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

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"

// 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")) {
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: 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>

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
Copy link
Copy Markdown
Contributor

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

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
Copy link
Copy Markdown
Contributor

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

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.

Copy link
Copy Markdown
Contributor

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

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:203">
P2: New thinking-state animations ignore `prefers-reduced-motion`; include these classes in the reduced-motion override to avoid continuous motion for users who request reduced animation.</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
box-shadow:
0 0 60px color-mix(in srgb, var(--brand-primary) 35%, transparent),
0 0 140px color-mix(in srgb, var(--brand-primary) 18%, transparent);
animation: orb-spin 6s linear infinite, orb-breathe 3.4s ease-in-out infinite;
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: New thinking-state animations ignore prefers-reduced-motion; include these classes in the reduced-motion override to avoid continuous motion for users who request reduced animation.

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

<comment>New thinking-state animations ignore `prefers-reduced-motion`; include these classes in the reduced-motion override to avoid continuous motion for users who request reduced animation.</comment>

<file context>
@@ -173,6 +173,93 @@ export const PAGE_PREVIEW_HOST_HTML = `<!doctype html>
+      box-shadow:
+        0 0 60px color-mix(in srgb, var(--brand-primary) 35%, transparent),
+        0 0 140px color-mix(in srgb, var(--brand-primary) 18%, transparent);
+      animation: orb-spin 6s linear infinite, orb-breathe 3.4s ease-in-out infinite;
+    }
+    @keyframes orb-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
</file context>

Tip: Review your code locally with the cubic CLI to iterate faster.

@vibegui vibegui force-pushed the vibegui/page-editor-agent branch from 6593dfb to 23c3395 Compare May 15, 2026 15:39
@cubic-dev-ai
Copy link
Copy Markdown
Contributor

cubic-dev-ai Bot commented May 15, 2026

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 and others added 21 commits May 15, 2026 15:01
… preview

Adds a local-first Page Editor agent that builds zero-build landing pages with
Claude Code, with a dedicated preview pane and a scaffolding pipeline that
splits pages from design systems.

Highlights:

- Storage layout under .deco/page-editor/: pages/<slug>/ (index.html, app.js,
  sections.js, page.js, meta.json) and design-systems/<slug>/ (tokens.css,
  tokens.js, demo.html, meta.json).
- New MCP tools: DESIGN_SYSTEM_CREATE / LIST / SET, PAGE_PREVIEW_PAGE_CREATE,
  plus the existing PAGE_PREVIEW_STATUS / SET / REFRESH. Scaffolding is
  template-driven so the agent doesn't hand-roll boilerplate.
- Templates: design-system demo (typography, swatches, buttons, cards, forms,
  spacing) and page layout (nav + hero + sections + footer). Tokens are CSS
  custom properties plus a JSON-encoded BRAND module. Staggered fade-in
  animation on every page section + design-system block.
- Preview pane (apps/mesh/src/web/layouts/main-panel-tabs/page-preview-tab.tsx):
  dual selector (page + design system) with the bound design system surfaced
  for the active page, fresh-chat defaults to the welcome quiz, an Export
  button, and an iframe that re-keys on file changes.
- Welcome quiz: 3-question card grid (what to build, vibe, audience) that
  composes a prompt and posts it to the chat input.
- Export: self-contained zip. index.html inlines the design system's
  tokens.css as <style> and consolidates tokens.js + sections.js + page.js +
  app.js into one inline <script type="module">, so the bundle is
  double-click-openable. Original multi-file source preserved under src/.
- Contrast enforcement: every brand passed to DESIGN_SYSTEM_CREATE is
  normalized via WCAG luminance math before tokens hit disk. fg >= 7:1
  against bg (AAA), muted >= 5.5:1, border >= 1.5:1; surface nudged for
  visible separation. Mixes toward fg to preserve hue. Fixes the recurring
  illegible-pastel-on-pastel failure mode.
- Robustness fixes hit along the way:
  - mcp-clients/client.ts: SELF (`<orgId>_self`) pseudo-connections now use
    an in-process MCP client over InMemoryTransport instead of a self-HTTP
    roundtrip. The previous HTTP path required Bun fetch to resolve
    `*.localhost`, which it cannot on macOS — so the virtual MCP's tool
    list never reached Claude Code.
  - lazy-client.ts: bypass the NATS SWR cache for in-process MCP servers so
    newly-added management tools surface immediately.
  - templates.ts: render tokens.js via JSON.stringify so font stacks
    containing quotes can never produce SyntaxErrors; tokens.css font
    interpolation is normalized via a small helper.
  - app.js template wraps each section in a Preact ErrorBoundary so a
    single broken section can't blank the whole page.

Includes:
- unit tests for the service (storage, scaffolding, export, contrast
  enforcement) and contrast math (24 tests, 77 assertions)
- a closed-loop integration script (scripts/test-page-preview-mcp.ts) that
  mints a Claude-Code-style API key and drives the live MCP virtual-mcp
  endpoint end-to-end (initialize → tools/list → DESIGN_SYSTEM_CREATE →
  PAGE_PREVIEW_PAGE_CREATE → REFRESH → /export zip)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The agent now narrates its build via a new PAGE_PREVIEW_PROGRESS({ label })
MCP tool. The preview pane renders an animated overlay above the iframe
keyed off the label, and any scaffold/refresh tool (DESIGN_SYSTEM_CREATE,
PAGE_PREVIEW_PAGE_CREATE, PAGE_PREVIEW_SET, PAGE_PREVIEW_REFRESH) clears
the label so the new state reveals.

  PROGRESS "Picking a design system…"  → overlay
  DESIGN_SYSTEM_CREATE                 → overlay clears, demo fades in
  PROGRESS "Building page structure…"  → overlay
  PAGE_PREVIEW_PAGE_CREATE             → overlay clears, page shell shows
  PROGRESS "Designing the hero…"       → floating pill
  Edit + PAGE_PREVIEW_REFRESH          → section reveals (staggered fade)
  …

- New tool PAGE_PREVIEW_PROGRESS registered in tools/index.ts +
  registry-metadata.ts; included in the Page Editor recruit modal's
  selected_tools.
- State persistence: state.json now carries progressLabel +
  progressUpdatedAt so the overlay survives a tab reload mid-flight.
  Every scaffold/refresh handler clears both fields.
- Overlay: two variants. `full` covers the whole pane with a soft radial
  backdrop + centered pill when the iframe is still the welcome quiz;
  `floating` is a bottom-center backdrop-blurred pill that sits over the
  live preview so the user can still see the page taking shape. Smooth
  crossfade between labels via a 320ms displayed-label trailing state.
- System prompt rewritten with an explicit step-by-step pattern: every
  visible unit of work is preceded by a PROGRESS call. Label-writing
  rules (3–6 words, gerund/imperative, end with '…', concrete not
  generic) and bad/good examples. Stage-3 section edits each get their
  own PROGRESS announcement.
- Service test for the new tool (label set then scaffold/refresh clears).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stops the "ugly intermediate page" effect by holding the preview on the
design system until the first real section is in place, and by making
the page start empty so the agent can append one block per refresh.

Service:
- PAGE_PREVIEW_PAGE_CREATE no longer auto-activates the preview by
  default. Adds an `activate` flag (default false). The page is
  scaffolded but the design-system preview stays in view until the
  agent edits page.js and calls PAGE_PREVIEW_SET.
- Closed-loop integration test updated; new unit test covers
  `activate: true` opt-in path.

Templates:
- page.js ships as `export const PAGE = [];` — the agent appends one
  block per Edit.
- app.js renders an EmptyPageState (centered pulsing icon + brand
  copy) when PAGE is empty, instead of a fully blank body.
- sections.js bundles a full library: Nav, Hero, FeatureGrid,
  PricingCards, TestimonialQuote, LogoStrip, FAQ, EmailCapture,
  CTASection, Footer, plus the existing PlaceholderSection escape
  hatch. The agent only needs to add page.js entries; no need to
  rewrite sections.js per page.

Export bundle:
- Fixes "Uncaught SyntaxError: Identifier 'h' has already been
  declared". Each chunk used to carry its own
  `import { h } from 'preact'` + `const html = htm.bind(h);`;
  concatenating verbatim re-declared them. Added stripAllImports() +
  stripHtmBindLine() helpers and hoist a single canonical preact/htm
  import + html binding at the top of the consolidated inline
  module. Same fix for the design-system export.
- Tests assert exactly one preact import and one html binding in the
  resulting index.html.

Prompt:
- Rewritten opening sequence: PROGRESS → DESIGN_SYSTEM_CREATE →
  PROGRESS → PAGE_PREVIEW_PAGE_CREATE → Edit page.js (first block) →
  PAGE_PREVIEW_SET → PROGRESS → Edit → REFRESH for each subsequent
  block.
- New "ONE BLOCK PER EDIT" rule explicitly forbidding wholesale
  rewrites of sections.js, batched edits, and preliminary Read calls.
- Stage 3 documents the full sections library and the expected
  6–8 PROGRESS/Edit/REFRESH triples for a landing page.

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

Runtime errors in the preview (SyntaxError in sections.js, uncaught
throws during render, unhandled promise rejections, …) used to fail
silently — the user saw a blank dark page and had to open devtools.

Now the page template registers a top-level error handler that:

  - Captures window.error and unhandledrejection events.
  - Renders a brand-themed full-screen error card with the headline
    ("SyntaxError in the preview"), file:line:col location stripped of
    the /api/<org>/page-preview/files/ prefix, and the actual message
    in a monospace block. Card uses the current --brand-bg / --brand-fg
    / --brand-primary so it always matches the page's design.
  - Offers two buttons:
      * "Ask the agent to fix this" — postMessages a structured
        payload (headline / location / message) to the parent window.
        PagePreviewTab listens for type "page-editor:runtime-error",
        composes a chat-input prompt instructing the agent to open the
        file, fix the bug, and call PAGE_PREVIEW_REFRESH. The user
        just hits Send.
      * "Reload preview" — location.reload() in the iframe.

First error wins; subsequent errors during the same load are ignored
so the card doesn't flicker between cascading failures.

Existing pages on disk regenerated to pick up the new index.html.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The agent was dragging unrelated pages into the preview iframe by
calling PAGE_PREVIEW_STATUS at the start of a build, reading old
pages/<slug>/ files, and PAGE_PREVIEW_SET-ing to a stale slug. The
preview pane followed state.json blindly, so the iframe ended up on
"System Health Agent" / "deco Finance Agent" pages from prior
sessions while the agent was supposedly building a brand-new page.

Two-pronged fix:

UI — preview-pane filtering:

- New deriveSessionItems() walks the chat stream and surfaces only
  the design-system slug from this chat's DESIGN_SYSTEM_CREATE call
  and the page slug from this chat's PAGE_PREVIEW_PAGE_CREATE call.
- The iframe shows only those session items (or what the user
  explicitly clicked in the dropdown). PAGE_PREVIEW_SET to an
  unrelated slug is ignored; PAGE_PREVIEW_SET that matches the
  session page activates the page; PAGE_PREVIEW_REFRESH always
  activates the session page.
- When the chat stream has NO preview-tool signal (fresh tab reopen,
  no agent activity yet), we fall back to status.* — but only then,
  so previously-built items still show up if the user just reloads.
- showKind / activePage / activeDs all rewired around the session
  items; the design-system dropdown still surfaces all on-disk
  systems so the user can browse, but the live iframe is locked to
  the session.

Prompt — session isolation rules:

- New top "Session isolation — non-negotiable" section that
  explicitly forbids PAGE_PREVIEW_SET / Read on pages the agent
  didn't create this turn, forbids PAGE_PREVIEW_STATUS at the start
  of a build, and reminds the agent to pick a fresh slug derived
  from the user's prompt (with discriminators on collision).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The exported index.html was throwing
  ReferenceError: Sections is not defined
because the inline-bundling pipeline strips
  import * as Sections from './sections.js';
but app.js references Sections[block.section] — and after stripping
`export ` from each function declaration in sections.js, the
individual Nav/Hero/etc. are loose top-level bindings with no
namespace object collecting them.

Fix: parse the export names out of sections.js (export function,
export const, export class, export let, export var, with optional
async) and emit a literal
  const Sections = { Nav, Hero, FeatureGrid, … };
right after the sections.js chunk in the consolidated inline module.

Tests assert the synthesized namespace is in the output and that
spot-checked section names (Hero, Nav) land inside it. Verified
end-to-end against the user's actual on-disk page export: 1 preact
import, 1 htm.bind(h), Sections namespace present, no leftover
`export ` keywords, the inline module parses as valid JS.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The iframe used to point at each page's index.html directly, with
hard reloads on every refresh and a 1:1 mapping between
"design-system selected" and "iframe URL". That made every transition
a page-load and forced "switch DS" to feel like "open a new tab".

New model: a single Studio-controlled HOST html, served at
/api/<org>/page-preview/host, stays mounted for the whole session.
It runs a preact render loop, dynamically imports the page's
tokens.js / sections.js / page.js from /files/... on demand, and
takes commands over postMessage from the Studio side:

  host:welcome                    show the welcome quiz (now living
                                  inside the host as a preact component)
  host:set-page                   load and render a page with section
                                  reveal animation
  host:refresh-page               re-import current page modules; only
                                  newly-introduced blocks animate in
  host:retheme                    apply a different DS's brand tokens
                                  to the current page (no reload, just
                                  CSS-variable transitions)
  host:show-design-system         render the DS gallery inline as preact
  host:show-design-system-grid    render a grid of all design systems
                                  as clickable cards
  host:update-design-systems      keep the host's cache fresh
  host:hello                      hand the host its files-base URL

Host emits back:
  host:ready                      handshake
  page-editor:prompt              welcome-quiz submission
  page-editor:host-select-ds      user picked a card from the grid
  page-editor:host-close-ds-grid  user closed the grid
  page-editor:host-request-refresh user reloaded after an error
  page-editor:runtime-error       window.error / unhandledrejection

Transitions are real now:
- Mode crossfade: 240ms opacity+blur fade on the stage container when
  switching between welcome / page / ds-demo / ds-grid.
- Section reveal: host tracks already-rendered section keys; only
  newly-introduced sections animate in (staggered 0/90/180/270 ms)
  so each incremental Edit+REFRESH produces a clean append.
- Brand retheme: changing tokens animates via CSS transitions on
  --brand-bg / --brand-fg / etc. (320ms cubic-bezier).

UX changes:
- DS dropdown click now RETHEMES the current page instead of
  replacing it. The dropdown's label reflects the *effective* DS
  (override > page-binding) so the user can see what's applied.
- Added a "Manage design systems" entry at the bottom of the DS
  dropdown that switches the host into the grid view. Clicking a
  card from the grid retheme's the current page.
- iframe is stable (loaded once, never re-keyed except by explicit
  Reload action) so postMessages survive scaffold/refresh tool
  cycles.

Server:
- /api/<org>/page-preview/host serves the host HTML.
- CSP middleware widened to permit framing for both /files/* and
  /host (X-Frame-Options: SAMEORIGIN, frame-ancestors: 'self').

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three bugs the user hit in quick succession against the new host
architecture, plus backwards-compat for legacy pages:

1. First load broke when the chat stream referenced a design-system
   slug that did not exist on disk. The host dynamically import()s
   tokens.js for that slug; 404 → TypeError. Fix:
   - deriveSessionItems now ignores tool calls whose state isn't
     exactly "output-available" (output-error was sneaking in).
   - The Studio side also drops session slugs that don't resolve to
     a real entry in status.pages / status.designSystems. The
     dropdown's items are always real, which is why manual picks
     worked even when the agent's session slug was bogus.

2. Switching pages via the dropdown didn't work. handleSelectPage
   bumped refreshNonce, which was wired into the iframe key, which
   remounted the iframe, which dropped the host's ready state, which
   silently swallowed the next set-page postMessage. Fix:
   - iframe key is now a stable constant ("page-preview-host").
     refreshNonce keeps driving the status query refetch but no
     longer touches the iframe.
   - "Reload preview" (from the host's error card) now reassigns
     iframe.src and resets hostReady + lastDispatchRef so the
     handshake replays cleanly without React remounting the node.

3. Switching design systems triggered a full page reload + crossfade
   when a retheme was wanted. Added a lastDispatchRef so the
   dispatch effect can tell "same page, just different DS" from
   "different page", and send host:retheme (CSS-variable transition
   only) instead of host:set-page in that case. Same idea for
   ds-demo → ds-demo with a different slug.

Backwards-compat for legacy pages:
- loadPage in the host now loads sections.js + page.js as hard
  dependencies, but treats tokens.js as best-effort. If the bound
  DS is gone (deleted, slug typo, pre-DS-split page), the host
  keeps the current brand variables and renders the page instead
  of blowing up. Old pages that just have sections.js + page.js
  still render.

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

Two stuck-state bugs on top of the host bridge:

1. First load: the host's filesBase was empty until Studio sent
   host:hello, but the dispatch effect (host:set-page) ran before
   the hello effect on the same hostReady→true tick, so dynamic
   imports resolved to root-relative URLs and 404'd.

   The host now derives filesBase from its own location.pathname
   (strips trailing /host) at boot, so dynamic imports work from
   the very first message — no handshake required. host:hello can
   still override if Studio knows better, but it's no longer a
   prerequisite.

2. Error card was sticky. Once it rendered for any reason, even a
   successful retheme / page-change kept it covering the screen
   (the host's "rendered" guard prevents re-firing but also
   prevents auto-dismiss).

   Added _maybeClearError() at the top of every Studio command
   handler. Successful host:retheme / host:set-page /
   host:refresh-page now dismiss the card. host:welcome and
   ds-grid already go through setMode which clears as before.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
handleSelectPage bumped refreshNonce, which invalidated the useQuery
key (queryKey includes refreshNonce), which set status to undefined
while the new query was in flight, which made pages an empty array,
which made overridePage = pages.find(...) === undefined, which made
activePage null, which made the intent fall through to "welcome".
The host received host:welcome → host:set-page in rapid succession
and the user ended up looking at the welcome quiz.

The dropdown items already came from the loaded status; refetching on
click adds zero new data, only the transient empty-status flash.
Removed the refreshNonce bump + refetch from both
handleSelectPage and handleSelectDesignSystem. Status now only
refetches in response to chat-stream changes, which is when new
data actually appears on disk.

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

Two complaints from the latest run, both UX:

1. 1m30s of welcome quiz with no visual feedback while the agent
   reads the brand context, plans, etc. Now: the moment the chat
   task starts and before any preview-state tool fires, the host
   switches to a new "thinking" mode that:
     - Shows the user's last prompt echoed in a soft card,
     - A glowing brand-tinted orb that breathes + slowly rotates,
     - Three pulsing dots + "The first section will appear here as
       soon as the agent starts writing."
   The moment ANY preview-state tool result lands (PROGRESS, the
   first DESIGN_SYSTEM_CREATE / PAGE_PREVIEW_PAGE_CREATE / SET /
   REFRESH), the intent transitions away from thinking and the
   normal page / DS-demo flow takes over — smooth crossfade.

2. PAGE_PREVIEW_SET to an existing on-disk page didn't update the
   iframe. My previous session-tracking only honored SET when the
   target slug matched a PAGE_PREVIEW_PAGE_CREATE earlier in the
   same chat — too restrictive. The agent legitimately SETs to a
   pre-existing page when iterating, and the preview should follow.
   New rule: extract the slug from the SET path (handles bare slug,
   slug/index.html, pages/slug/index.html, absolute paths), set
   sessionItems.pageSlug + pageActivated. Studio's existing
   on-disk filter still ignores typo / non-existent slugs.

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

When I added the thinking-intermission branch I shuffled the intent
checks and accidentally moved `isFreshChat && !hasStreamSignal` AFTER
the page / ds-demo branches. That let the status-fallback path
(status.activeKind === "page" from a previous chat's state.json)
win on a brand-new chat, so opening a fresh conversation would
immediately drop you into the last edited page instead of the
welcome quiz.

The fresh-chat welcome check must come before the status-fallback
branches. Restored the original order.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two fixes for the "agent says hero is in preview but iframe shows
welcome quiz" symptom:

1. Removed refreshNonce from useQuery's queryKey. Every tool call
   (especially PAGE_PREVIEW_PROGRESS) was bumping refreshNonce, which
   minted a new cache entry — status went undefined during each
   refetch, collapsing pages to [], dropping sessionPage/sessionDs to
   null, and falling intent through to the welcome branch until the
   new fetch resolved.

2. Hold the "thinking" intermission until we have concrete content
   (hasStreamSignal) rather than ending it on the first preview-state
   tool. The first PAGE_PREVIEW_PROGRESS used to flip thinking off
   before any DS/page existed, so the iframe dropped back to welcome
   for the gap. The progress overlay continues to render labels on top
   of the thinking screen, preserving the agent's narration.

Also split PREVIEW_STATE_TOOLS into DISK_MUTATING_TOOLS (drives the
refetch trigger; excludes PROGRESS) and the original set (drives
fresh-chat / thinking detection; includes PROGRESS).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When the agent appends a section and calls PAGE_PREVIEW_REFRESH, the
host now scrolls the first newcomer into view so the user follows
the build instead of staring at the same hero.

Detection reuses the existing reveal-animation mechanism: only DOM
nodes that weren't in seenSectionKeys get .section-enter, so the
scroll is naturally a no-op when nothing new appeared (brand-only
retheme, idempotent refresh). We wait two animation frames so
measurement happens after preact commits and after the entry
animation's initial transform is applied — otherwise scrollIntoView
lands on a stale pre-animation offset.

Honors prefers-reduced-motion.

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

Five polish wins that turn the "single hero on empty canvas" feeling
into a complete-looking page from t=0.

1. Persistent page shell (Nav + Footer). Renders brand-styled
   placeholders the moment we enter page mode, even with blocks=[].
   Hidden once the agent adds a real Nav/Footer section. A page with
   one hero no longer looks like an orphan widget.

2. "Drafting next…" slot. Subtle pulsing skeleton with shimmer that
   appears below the last block while the agent is working. Shows the
   current PAGE_PREVIEW_PROGRESS label so the empty space turns into
   anticipation.

3. DS → page bridge banner. When transitioning from a design-system
   demo into the page, briefly shows a brand color strip + "Built with
   <DS name>" pill. Tells the visual story instead of crossfade-then-
   strange-empty-screen.

4. Tactile section reveal. .section-enter keyframes now include
   scale(0.975→1) + a brief shadow lift around 60% so sections feel
   like cards being dealt rather than DOM nodes blinking in.

5. Outline-driven mini-TOC stepper. PAGE_PREVIEW_PROGRESS gained an
   optional outline:string[] argument. Persisted in state.json,
   piped through the chat stream to the host via a new
   host:set-page-progress bridge message. Renders as a sticky strip
   at the top of page mode showing done/current/planned section
   labels with a pulse on the current step. Agent prompt updated to
   declare outline on the first PROGRESS call.

Outline + progress label + isRunning all flow through one new bridge
message (host:set-page-progress); Studio derives the outline from the
chat stream first, falling back to status.outline on cold load.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The progress overlay was floating "Laying out the features grid…" on
a fresh recruit because the previous build's last progressLabel
persists in state.json. Studio fell back to status.progressLabel
whenever the chat stream had no messages, which is exactly the
fresh-chat case.

Gate the server-side fallback on isTaskRunning so the stale value is
ignored when nothing is happening. The fallback still kicks in during
a mid-build tab reload (task running, messages haven't loaded yet) —
its actual intended use case. Same gate applied to outline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI knip step caught:
- Unused file: apps/mesh/src/web/components/home/page-editor-welcome-html.ts
  (the welcome quiz now lives inline in the host iframe; standalone
  HTML constant was never wired up after that move)
- Unused exports: toHex, relativeLuminance, isLight (internal helpers
  in contrast.ts — drop export keyword)
- Unused export: PAGE_PREVIEW_HOST_MARKER (referenced only inside the
  same file's template literal — drop export)
- Unused export: discoverDesignSystems (called only by listDesignSystems
  and getPagePreviewStatus in the same file — drop export)
- Dead exports never called anywhere: resolvePagePreviewFile, listPages
  (delete)

Per CLAUDE.md, knip config is sacred — fix the code, not the config.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three fixes for "DS demo sits frozen while agent reads files":

1. Outline was being wiped server-side on DESIGN_SYSTEM_CREATE and
   PAGE_PREVIEW_PAGE_CREATE — but the agent declares its plan ONCE
   on the very first PROGRESS call, and that plan applies to exactly
   the DS+page about to be built. Wiping it dropped the stepper at
   the worst moment. Now both calls preserve the outline. Matching
   change in Studio's deriveLiveOutline.

2. The outline stepper now renders inside DS-demo mode too (when a
   task is running and outline is set). After PAGE_CREATE the
   preview stays on the DS demo for a while (agent reading scaffold
   files, writing the first section) and previously this looked
   indistinguishable from "agent is stuck".

3. Prompt update: explicitly require PAGE_PREVIEW_PROGRESS before
   any Read / Glob / Edit that happens between PAGE_CREATE and the
   first SET. Without these intermediate labels, the pill overlay
   has nothing to show and the DS demo looks frozen.

The host now rerenders for set-page-progress in both page AND ds-demo
modes so the stepper updates live during the in-between window.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two fixes for "page created but stuck in design system":

1. Prompt: explicitly forbid Read on page.js / sections.js after
   PAGE_CREATE. Their contents are documented in the prompt and are
   authoritative — the agent was conscientiously reading them anyway
   and adding 10-30 seconds of dead air per build while the user
   stared at the frozen DS demo.

2. Studio progress overlay: when isTaskRunning but progressLabel is
   null (the gap between PROGRESS calls during Edit/Read), show a
   generic "Working…" pill instead of disappearing. The user always
   sees the agent is alive, even between announcements.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Removes the separate "design system demo" iframe mode. The host now
renders one cohesive layout — shell (Nav placeholder + Footer) +
stepper + main + drafting slot — and the <main> content swaps as
the build progresses:

  blocks = 0 + brand → inline DS gallery + drafting slot
  blocks > 0          → real sections + drafting slot

The agent's flow is unchanged (DS_CREATE → PAGE_CREATE → Edit → SET).
What changes is the user's experience: the iframe enters page mode
the moment a DS is created and never leaves it. The DS gallery is
just the first "step" in the page, replaced by sections as they
land — no jarring mode transition.

Drops the now-unused DSBridgeBanner component, its CSS, the
bridgeUntil state field, the ds-demo state.mode value, and the
setTimeout-based banner cleanup. Bridge handler host:show-design-
system now enters page mode directly (clears blocks, applies brand).
host:set-page rerenders in-place when already in page mode so the
gallery → first-block transition keeps the shell anchored.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ship five fixes blocking the page-editor demo:

1. Recruit modal's update branch was dropping PAGE_PREVIEW_PAGE_CREATE,
   PAGE_PREVIEW_PROGRESS, and the three DESIGN_SYSTEM_* tools from the
   agent's selected_tools. Re-recruiting an existing Page Editor stripped
   the tools the system prompt mandates as its first calls, leaving the
   agent unable to invoke them. Unify both branches behind a single
   PAGE_EDITOR_SELECTED_TOOLS constant.

2. After PAGE_PREVIEW_PAGE_CREATE the agent reliably emitted a long
   prose plan and ended its turn without promoting the preview off the
   DS gallery. Add a `nextStep` advisory to the output of each
   chain-driving tool (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 land at the
   model's strongest attention point and bypass the long-prompt drift
   that the top-of-prompt rule wasn't catching. System prompt also
   gets a new "THE ONE RULE" section forbidding prose between tool
   calls.

3. Hero (and other sections) were rendering template defaults
   ("Build a beautiful page.") because the nextStep was citing made-up
   prop names. Rewrite the nextStep and system prompt with the actual
   contracts from templates.ts for all ten library sections
   (Nav, Hero, FeatureGrid, PricingCards, TestimonialQuote, LogoStrip,
   FAQ, EmailCapture, CTASection, Footer).

4. DESIGN_SYSTEM_CREATE's tool description claimed missing fields get
   "sensible defaults" — but defaultBrand() returns dark-neon indigo
   on near-black, which is not sensible for, say, a banana-themed
   landing page. The agent would call DS_CREATE with sparse brand,
   see the wrong colors flash, then re-call with the real palette.
   Tighten the description + system-prompt guidance to commit the
   full palette on the first call.

5. The outline stepper at the top of the preview persisted after the
   build finished. Gate it on isRunning so it fades out the moment
   the agent's turn ends.

Also fix a session-isolation bug: the activePage / showKind server-state
fallbacks fired whenever this chat had no session DS/page yet, including
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.
Gate both fallbacks on !previewToolFiredEarly so they only fire for true
cold loads.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vibegui vibegui force-pushed the vibegui/page-editor-agent branch from 1128b36 to aa7622e Compare May 15, 2026 20:58
vibegui and others added 3 commits May 15, 2026 18:24
Expand the welcome quiz from 3 required questions (kind / vibe / audience)
+ one trailing textarea to 7 fully-optional questions, each with cards
and an optional free-text "details" slot.

Synthesizes a competing-perspectives review (conversion fundamentals vs.
brand specificity / anti-slop):

  1. I'M BUILDING        — page kind (no free-text)
  2. THE PRODUCT IS      — product pattern + proper-noun free-text
  3. WRITTEN FOR         — audience + "what they're replacing" free-text
  4. ONE GOAL            — CTA pattern + exact-button-label free-text
  5. VOICE LIKE          — named-brand voice ref + free-text link/quote
  6. VISUAL ANCHOR       — wired to system-prompt design-language anchors
  7. PROOF I HAVE        — proof type + verbatim free-text, with explicit
                           "Nothing yet — do not fabricate" option

Voice is split off from visual: voice is what fights generic copy
(named-brand reference >> adjective vibes), visual is what fights
generic CSS. Color is folded into visual anchor.

Every detail field wraps the user's text as "use VERBATIM, do not
paraphrase, do not invent additions" so Claude treats it as ground
truth rather than hint. The PROOF question's "Nothing yet" option
explicitly tells the agent to skip LogoStrip/TestimonialQuote rather
than fabricate "Trusted by 10,000+ teams" social proof.

All questions optional — the Generate prompt button is always live.
Composer emits only the lines for questions actually answered, and
defaults to a minimal "Build a landing page." stub if nothing was
picked. Clicking a selected card a second time clears it.

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

The outline stepper at the top of the preview pane becomes a scrub bar:
clicking a done or current step pins the right pane to that historical
snapshot while the agent keeps working in the background. A "Live ›"
pill appears when pinned, restoring the live follow.

Concretely:

- A "Design system" step is prepended to the stepper (index 0). Clicking
  it rewinds the preview to the InlineDSGallery (colors, type, buttons,
  spacing) even after sections have started landing — so the user can
  re-admire the DS while the rest of the page assembles.
- Each subsequent step (Nav, Hero, Features, …) corresponds to "first N
  sections rendered". Click a done step and the right pane crossfades
  back to that state.
- Planned steps (ahead of the live cursor) stay inert. Can't time-travel
  to a future that hasn't happened.
- DraftingSlot (pulsing "Adding next…" placeholder) only renders in live
  mode — hidden when pinned to a past snapshot so the snapshot looks
  clean.
- <main> is keyed on the resolved view step, so a step click remounts
  the element and re-triggers a 280ms fade-in.
- viewStepIdx clears on mode transitions so a pin from one chat doesn't
  bleed into the next.

CSS adds a new .toc-item-viewing state (inverted fg/bg pill so the
viewed step pops), .toc-item-clickable (cursor pointer + lift-on-hover),
.toc-item-rewindable (a small ↻ glyph hint on past clickable steps),
and the .toc-item-live "Live ›" pill that pulses while pinned.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When the preview iframe catches a runtime error (SyntaxError, broken
section, undefined identifier, etc.) and the chat task is idle, send
the structured error report directly via chatStream.sendMessage()
instead of just composing it into the input. The agent picks it up
like any other prompt and starts fixing immediately — no user click
required.

Gates / dedupe:

- Only auto-bubble when the agent's turn has finished. While the task
  is in_progress or expired we don't pile on another user message;
  let the current turn finish first. (The in-iframe error card still
  shows, so the user can manually escalate via "Ask the agent" if
  they want to interrupt.)
- A dedupe ref (lastAutoReportedErrorRef) holds the signature of the
  most recently auto-reported error (headline + location + first 240
  chars of message). Identical errors that fire again before the
  agent has had a turn do not re-send.
- The dedupe clears the moment the chat task transitions back to
  idle. So if the agent's fix didn't work and the next refresh re-
  errors with the same signature, the auto-bubble re-fires. Loops
  on the same error only happen because the agent keeps trying and
  failing — user can always interrupt.
- Fallback to composeChatInput() when there's no active chat stream
  (no agent task open) or auto-bubble was skipped — preserves the
  manual "Ask the agent" flow.

Live values (stream + taskStatus) reach the window message listener
via mutable refs assigned during render, since the listener is
registered once on mount and can't read live React state directly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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