Skip to content

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

Open
vibegui wants to merge 64 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 64 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 design systems, a stable host‑iframe live preview, a time‑travel stepper, and zero‑build export. This update unifies the early preview into a single design phase and paces section reveals for a calmer build flow.

  • New Features

    • Unified Design phase: split‑screen DS gallery (left) + 24‑section library (right), with the stepper simplified to "Design" → outline and a paced reveal queue (1.5s between sections). Honors reduced‑motion.
  • Bug Fixes

    • Footer settles in place at the end: no center‑scroll on the last section, and the bottom spacer collapses as soon as the outline is complete; brief hold, then scroll to top.
    • Refreshing a finished page is read‑only: no reveal animations or scrolls, and design/library phases are skipped.

Written for commit 0260fec. 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.342.0) — based on feat: prefix

React with an emoji to override the release type:

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

Current version: 2.341.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/api/routes/proxy.ts">

<violation number="1" location="apps/mesh/src/api/routes/proxy.ts:98">
P2: Dev-assets support was added for `/:connectionId` but not for `/:connectionId/call-tool/:toolName`, so direct call-tool requests against `{org}_dev-assets` still return 404.</violation>
</file>

<file name="apps/mesh/src/api/routes/page-preview.ts">

<violation number="1" location="apps/mesh/src/api/routes/page-preview.ts:127">
P2: Do not return raw internal error messages from `/export`; this can leak server filesystem details. Return a sanitized message instead.</violation>
</file>

<file name="apps/mesh/src/mcp-clients/client.ts">

<violation number="1" location="apps/mesh/src/mcp-clients/client.ts:53">
P1: The new SELF detection is too broad: `endsWith("_self")` can misroute non-SELF user connections to the in-process management MCP.</violation>
</file>

<file name="apps/mesh/src/page-preview/service.ts">

<violation number="1" location="apps/mesh/src/page-preview/service.ts:1051">
P1: Escape `</script>` before embedding inline module code in exported HTML to prevent script-breakout injection.</violation>
</file>

<file name="apps/mesh/src/web/layouts/main-panel-tabs/page-preview-tab.tsx">

<violation number="1" location="apps/mesh/src/web/layouts/main-panel-tabs/page-preview-tab.tsx:668">
P2: The design-system sync effect only watches `designSystems.length`, so metadata changes (name/brand edits) are missed and the host grid can display stale data.</violation>

<violation number="2" location="apps/mesh/src/web/layouts/main-panel-tabs/page-preview-tab.tsx:715">
P1: The iframe sandbox currently allows both scripts and same-origin, which effectively defeats sandbox isolation for same-origin generated preview content.</violation>
</file>

<file name="apps/mesh/src/page-preview/host-html.ts">

<violation number="1" location="apps/mesh/src/page-preview/host-html.ts:381">
P2: Do not swallow all `tokens.js` import errors; only ignore true “missing module” cases and rethrow other failures so broken design-system code is visible.</violation>
</file>

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

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
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 vibegui force-pushed the vibegui/page-editor-agent branch from 1128b36 to aa7622e Compare May 15, 2026 20:58
vibegui and others added 21 commits May 20, 2026 10:04
… 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 and others added 30 commits May 20, 2026 21:02
The soft prompt directive "DO NOT call BRAND_CONTEXT_*" didn't stick —
the agent kept calling BRAND_CONTEXT_LIST first on every build (30-60s
of "Pulling brand context…" dead air) despite explicit instructions.
Soft prompts aren't enough; remove the tools from the allowed list so
the agent literally can't call them.

Trade-off: the rare legitimate brand-context flow breaks. If a user
needs a saved brand, they can re-enable BRAND_CONTEXT_LIST / GET /
EXTRACT in PAGE_EDITOR_SELECTED_TOOLS — but that's the exception, not
the default. The curated DESIGN_SYSTEM_TEMPLATES_LIST (10 themes,
contrast-checked) covers the common path.

Also trims the now-redundant brand-context paragraph from the system
prompt's opening sequence — replaced with a tight reminder to pick a
template by slug from DESIGN_SYSTEM_TEMPLATES_LIST.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
47-second wait until first preview render. Bottleneck wasn't tool
roundtrips — it was the model THINKING. The system prompt had grown
to 332 lines of mostly-stale advice (Edit page.js / PAGE_PREVIEW_SET
/ PAGE_PREVIEW_REFRESH rhythm from the file-based flow, anti-
example tables, contrast rules, ONE-BLOCK-PER-EDIT rule, etc.) — all
irrelevant to the browser-as-REPL path the agent is on now. Plus
13 tools in the allowed list, each costing deferred-schema fetches
on first use.

Two cuts:

1. PAGE_EDITOR_SELECTED_TOOLS reduced from 13 → 6. Just the tools the
   new flow needs:

     PAGE_PREVIEW_PROGRESS
     DESIGN_SYSTEM_CREATE
     PAGE_PREVIEW_PAGE_CREATE
     PAGE_RENDER_BLOCK
     PAGE_UPDATE_BLOCK
     PAGE_REMOVE_BLOCK

   Out: DESIGN_SYSTEM_TEMPLATES_LIST (slugs are in the prompt),
   DESIGN_SYSTEM_SET / LIST, PAGE_PREVIEW_SET / REFRESH / STATUS
   (legacy file-based), BRAND_CONTEXT_* (already removed earlier).
   The agent has fewer schemas to fetch and fewer choices to
   reason over.

2. System prompt rewritten — 332 lines → ~90 lines. Removed:
   - The entire file-based flow (Edit page.js / Read page.js /
     PAGE_PREVIEW_SET / REFRESH / one-block-per-Edit rule).
   - Anti-pattern lists describing what NOT to do (the tools are
     just absent now; can't anti-pattern with what you don't have).
   - Brand-context paragraph.
   - htm syntax notes (only relevant for Stage-3 file edits which
     no longer happen).
   - Pages-dir discussion (the agent doesn't touch files).

   Kept the essentials, tighter:
   - Mission line.
   - 6-step build sequence (3–5 outline labels → DS_CREATE template
     → PAGE_CREATE → RENDER_BLOCK per section).
   - Curated themes table (10 slug+vibe rows).
   - Section prop contracts (one-line per section).
   - Hard rules (no chat between calls, no Read/Edit/Write/Grep/
     Glob/Bash/ToolSearch, one section per RENDER_BLOCK, end-
     turn after last section).

Expected: first DS_CREATE within ~5–10 s instead of 47 s. Less to
read on first turn = faster model warm-up; fewer tools = fewer
deferred-schema fetches; tighter sequence = less planning time.

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

Three regression fixes from the REPL flow rollout:

1. CRITICAL — "Unknown section: Hero / FeatureGrid / ..." rendering in
   red for every block. The new flow skips host:set-page entirely, so
   loadPage() never ran and state.Sections stayed empty. Every block
   fell into the "Unknown section: X" fallback branch in PageView.

   Fix: pass slug + designSystem on every host:render-block /
   update-block / remove-block postMessage. The iframe lazy-loads
   sections.js on the first render-block via the existing loadPage()
   helper, merges component refs into state.Sections, and stamps
   pageSlug / pageDsSlug for subsequent calls.

2. .shell-nav-cta hardcoded `color: white` over `background:
   var(--brand-primary)`. On cyber-lime (#D0EC1A), white-on-lime is
   ~1.5:1 — unreadable. Use var(--brand-on-primary) which is
   contrast-derived at DS-create time.

3. Welcome quiz composer was still appending "Pull my brand context
   if it is available; otherwise pin tokens to the visual anchor
   above. Pass ALL brand fields to DESIGN_SYSTEM_CREATE on the first
   call." to every prompt — but BRAND_CONTEXT_* tools are no longer
   available to the agent, and the visual anchor advice is now
   inside the system prompt. Drop the line.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User report: 47-50s before the first preview render, even on Opus.
The model spends that time reading the system prompt and planning
the page before any tool call lands. To the user, the screen is
frozen.

Add THE PARANOIA RULE at the top of the prompt: the model's very
first action, before any thinking, is:

  PAGE_PREVIEW_PROGRESS({ label: "Starting…" })

No outline, no theme, no analysis of the user's prompt. ~30 tokens,
~0.5 s on any model. The user sees the "Starting…" pill before the
model has even decided what to build. Then the model can think for
the next single step — picking the outline, calling PROGRESS again
with it, then DS_CREATE, etc.

Rephrased the build sequence: instead of "step 1 PROGRESS(label,
outline)", split into "step 1: instant PROGRESS('Starting…')",
"step 2: PROGRESS with outline once you've picked it". The outline
selection no longer blocks the first feedback to the user.

Also enforces "think one step ahead, never more" — don't pre-plan
section copy; decide Hero's props, ship it, then think about
Features. Keeps each model decision small and fast.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…on thinking view, DS full-bleed

UX choreography polish per user feedback:

1. Merged "layout" + "components" phases into one. Previously the
   layout phase showed shell + empty middle for 1.2 s, then the
   components phase added stub cards. Now both happen together: the
   moment the design-system minimum elapses, the shell grows in AND
   the stub cards appear in the middle. The transition is one beat,
   not two — feels less stuttery.

2. DS-phase main fills the full viewport. New .page-main-ds class
   with min-height: 100vh + flex column. The InlineDSGallery now
   reads as a full-bleed loading screen rather than a square card.

3. OutlineStepper now also renders on the thinking view (the
   intermission shown before any DS exists). As soon as the agent's
   second PAGE_PREVIEW_PROGRESS lands with an outline, the stepper
   shows up at the top of the brief — gives an immediate sense of
   "the page is already being planned" without waiting for DS.

4. PHASE_MIN_MS shrunk to just { designSystem: 2200 } — the layout
   and components min-hold durations are gone. The DS hold went
   from 2000 → 2200 ms, a touch more time to read the tokens
   before the page assembles around them.

5. Single rerender scheduler point: only the DS-hold boundary
   triggers a future rerender; the layout → building transition
   happens naturally whenever the first block arrives.

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

- PAGE_BOOTSTRAP collapses DS+page+outline+activate into one tool call
- PAGE_REVIEW_SUGGEST renders glassy tooltips pinned to sections with
  Accept/Dismiss; Accept fires the suggestion back via the new chat-input
  submitter bridge (composeAndSubmitChatInput)
- Sequential per-section rhythm, structural Footer terminator, duplicate
  rejection, fail-fast on unknown sections with closest-match suggestion
- SDK-level disallowedTools for page-editor agents (Read/Write/Edit/Bash/
  Grep/Glob/ToolSearch/NotebookEdit/WebFetch/WebSearch) so textual rules
  are backed by hard enforcement
- gridColsFor() prevents orphan 3+1 layouts at common preview widths
- A: stuck "Reviewing the page…" pill after F5 — gate the fallback on
  isTaskRunning so the rehydrated chat stream doesn't keep the pill on
- B: prevent page-flash on new arrivals — onAnimationEnd strips
  .section-enter from the DOM so a parent re-render can't re-trigger
  the keyframes on already-revealed sections
- C: center the new section in the viewport instead of pinning to top
  under the stepper; drop scroll-margin-top now that block:'center'
  takes care of the layout
- D: accept `suggestion` as a fallback alias for `prompt` in
  deriveReviewTips; add a tiny tips:N active:M diagnostic badge in the
  iframe so we can see whether tooltips actually reached the runtime
- Register PAGE_BOOTSTRAP, PAGE_REVIEW_SUGGEST and the rest in
  ALL_TOOL_NAMES so tools/index.ts typechecks
- The agent now ends with a question instead of running the review pass
  automatically. PAGE_REVIEW_SUGGEST only fires after the user opts in
  with yes. nextStep on the final RENDER_BLOCK and the system prompt
  both teach the new closing-question shape.
- Add padding-bottom: 60vh on .page-main so the last section has scroll
  room and scrollIntoView({ block: 'center' }) can actually center it
  instead of clamping to the document bottom.
The 60vh headroom is only needed while sections are still landing —
scrollIntoView({ block: 'center' }) needs the room to actually scroll.
Once the agent ends its turn the spacer is dead weight and looks like
a giant empty block below the footer.

Gate the padding on .is-building, tied to state.isRunning, so the
finished page reads naturally to the footer while the in-flight build
still gets the headroom that makes center-scroll work.
…t, robots.txt

Lift the page-editor output toward AI-citability after surveying
~/Projects/geo-seo-claude. Three layers, all automatic — the agent
doesn't get extra tools or extra work; better output falls out of
better prompt rules + a smarter export pipeline.

System prompt (page-editor-recruit-modal.tsx)
- New "Copy contract" section: definition-pattern Hero subtitle
  ("<Brand> is <category> that <verb>. <quantified differentiator>."),
  specific-over-abstract rule with exact-numbers enforcement, and an
  explicit ban list for AI-slop language (no "Transform your X", no
  "ever-evolving landscape", no hedging like "many/most/studies show").
- Per-section copy rules: FAQ answers 60–160 words self-contained
  answer-first; StatStrip exact numbers with scope; ProblemSolution
  question-phrased problem titles; PricingCards exact prices; FAQ
  ships ≥4 items; etc.
- Hard rule reminding the agent that JSON-LD / llms.txt / robots.txt
  are emitted automatically — focus on copy.

Semantic HTML (templates.ts)
- Hero subtitle gets .lede class so speakable JSON-LD can target it.
- Hero stats + StatStrip switch from <div> to <dl>/<dt>/<dd> so each
  number-label pair is extractable as a key-value fact.
- FAQ items use <h3> for questions and .faq-answer class on answers
  so speakable selectors can pull them.

Export bundle (service.ts)
- buildPageExportBundle now also emits:
  - JSON-LD @graph in <head>: Organization (with description from
    Hero subtitle + knowsAbout[] from FeatureGrid titles) + WebSite
    + WebPage with speakable selectors targeting .lede + .faq-answer,
    plus FAQPage when the page has a FAQ block.
  - /llms.txt: H1 + blockquote + sections (About, Features, Pricing,
    FAQ, Key Facts, Contact) derived from the block tree.
  - /robots.txt: explicit Allow for GPTBot, OAI-SearchBot, ChatGPT-User,
    ClaudeBot, PerplexityBot, Amazonbot, Google-Extended, Bytespider,
    CCBot, Applebot-Extended, FacebookBot, Cohere-ai; plus the emerging
    Content-Signal directive.
- New parseTokensJsBrand / parsePageJsBlocks helpers that round-trip
  the JSON literals already written to tokens.js / page.js so we can
  rebuild structured data from the exported artifacts without a second
  source of truth.
After surveying inferen-sh/skills/landing-page-design (conversion
mechanics) and anthropics/skills/frontend-design (aesthetic boldness),
three additions to the system prompt:

- Recommended outline shape — the proven Hero → Social proof → Problem
  → Solution → How → Testimonials → Pricing → FAQ → Final CTA funnel,
  mapped onto our section library with skip rules. Stops the agent
  freelancing odd orderings (Pricing before Problem, etc.).
- Hero headline + CTA formulas — 5 headline patterns
  ("[Outcome] without [pain]", "Stop X. Start Y.", etc.) plus an
  explicit ban list for low-commitment CTA labels ("Submit", "Click
  here", "Learn more", "Get started" without specifics). Forces the
  agent to actually commit to an outcome instead of writing
  "Welcome to <Product>".
- Aesthetic-commitment line under the theme table so the agent stops
  defaulting to electric-indigo for every brief. The bold themes
  exist; pick the one that fits the edge of the brand.

These complement (don't replace) the GEO copy contract that landed
last commit: GEO controls extractability; these control conversion
and visual distinctiveness.
Two changes after surveying nextlevelbuilder/ui-ux-pro-max-skill.

DS gallery — fit on one large-desktop screen (~700–800 px tall)
- Replace the vertical stack of six full-bleed sections (~1500-2000 px
  total) with a 3-column dashboard:
    [── DS hero (name + tagline) ───────────────]
    [Color] [Typography] [Shape (NEW)]
    [───── Cards (3 wide) ──────] [Buttons]
    [Form & Spacing ──────────────────────────]
- Color: 4×2 grid of small chips (36 px) with name + hex labels,
  replacing the 96-px chips that ate half the screen.
- Typography: 3 lines (display / body / caption) instead of 5.
- Shape: NEW tile showing four radius variants (0, ½, 1×, 2× of
  --brand-radius) so the user can see how the theme shapes surfaces
  before any section lands.
- Cards: compact padding, narrower copy.
- Form & Spacing: collapsed into a single tile (2-input row + 4
  spacing samples).
- Falls back to a single column under 960 px so smaller previews still
  read cleanly.

System prompt — conversion placement rules (ui-ux-pro-max landing.csv)
- Sticky Nav CTA mirrors the Hero primary (same label, same destination).
- Social proof goes right after the Hero, never buried.
- Testimonials precede Pricing.
- CTASection at the bottom repeats the Hero CTA copy.
- Pick ONE landing archetype (hero+features+cta / pricing-focused /
  trust+authority / bento-showcase); don't mix.
- Visual consistency: one icon family across the page, one tone across
  sections.
Library grows from 14 → 24 sections. The new ones cover internal narrative
(memos, OKR docs, strategy briefs, blog posts), light data viz, and decision
pages — useful directly out of the box for the same generator that ships
landing pages today.

New section components (templates.ts):

Data / metrics / OKRs
- MetricsGrid — KPI/OKR cards with label, current, optional target, and an
  auto-computed progress bar.
- Timeline — vertical milestone list with date + title + body per item.
- Chart — minimal horizontal bar chart (pure CSS, no chart library) that
  normalizes to the largest value or an explicit max.

Narrative / memo / strategy
- Callout — boxed note with variant flag (info/tldr/warn/success/risk) for
  decisions, risks, and asides.
- KeyTakeaways — TL;DR bullet list pinned above long-form content.
- LongFormBody — multi-paragraph article prose, 64ch max, body-font lead.
- Byline — author + role + date + tags strip.

Comparison / promo
- Comparison — feature matrix table with optional highlighted column;
  boolean values render as ✓ / —.
- BeforeAfter — split-panel transformation card (state change, not pain→fix
  like ProblemSolution).
- Banner — top-of-page announcement strip with variant + optional CTA.

GEO integration (service.ts):
- buildJsonLdGraph now emits Article schema when a Byline block is present,
  pulling headline from Hero/LongFormBody title, author + datePublished from
  Byline, description from LongFormBody first paragraph. Generic landing
  pages still skip Article (would have required fabricated dateModified).
- buildLlmsTxt now folds Timeline into ## Roadmap, MetricsGrid current
  values into ## Key Facts, and LongFormBody first paragraph into a new
  ## Article section.

System prompt (page-editor-recruit-modal.tsx):
- Sections list split into "Landing-page sections" and "Beyond-landing
  sections" with prop contracts for each new one.
- Four typical outline shapes documented: memo, blog post, comparison page,
  product roadmap. The agent picks one library cleanly — no mixing.

KNOWN_SECTION_NAMES updated; the agent's typo-suggestion regex picks up the
new names automatically. The export bundle, JSON-LD, llms.txt all flow
through unchanged.
…0s hold

DS gallery — fix cramped layout you flagged in the screenshot
- Compact one-line hero (brand name + eyebrow on a single baseline)
  replaces the tall H2 + descriptive paragraph that ate vertical space.
- 3-col grid is now adaptive: 3 cols at ≥1100 px, 2 cols at 720–1099 px
  (the typical split-pane case the screenshot shows), 1 col below 720.
  Most tiles get more breathing room when the iframe is the narrower
  half of a Studio split.
- Cards row spans full width (was span 2). Form + Spacing combined into
  a single tile spanning 2 columns with the two facets side by side.
- Shape tile no longer renders 4 identical filled squares — it shows
  the radius applied to actual UI primitives (tag, button, block) so
  the rounding reads as a real design choice.
- Color swatches grew from 36 px → 48 px tall, with name + hex below.
- Tile inner padding bumped from space-4 → space-5.

Scroll regression — intermediate sections weren't being centered
- scrollToFirstNewcomer used querySelector('.section-enter') which
  returns the FIRST match in DOM order. When sections arrive faster
  than the 620 ms entry animation completes, multiple have the class
  simultaneously and the FIRST one is the OLDEST still-animating
  section near the top of the page, not the newcomer. Fix: pick the
  LAST .section-enter — the newcomer is always last in DOM order
  because RENDER_BLOCK appends.

DS hold — bumped from 7 s → 10 s
- "Flashed by too fast" feedback. The hold runs from applyBrand to
  the first phase transition, and 7 s felt rushed once the gallery
  had real content to read. 10 s gives clearer dwell time without
  blocking the build for noticeable extra time.
…e contrast

Scroll — replace scrollIntoView({block:'center'}) with explicit window
.scrollTo() that computes the target Y from getBoundingClientRect +
viewport height. Three reasons block:'center' was failing intermittently:

1. The 20% tolerance early-return was skipping intermediate scrolls
   when a section happened to land near the center band — common
   when sections vary in height.
2. block:'center' silently clamps to documentScrollHeight when the
   doc isn't tall enough to honor the centering, so even with the
   60vh build spacer the last few sections weren't moving.
3. Race with state.isRunning — Studio's host:set-page-progress can
   arrive after host:render-block; if isRunning was still false at
   the moment of render, .is-building wasn't on <main>, no spacer,
   no scroll room. revealBlock now sets state.isRunning = true
   defensively so the spacer is guaranteed during reveals.

DS gallery — visible tile separation on dark themes
- color-mix lifts the tile background a few % toward fg so tiles
  read as distinct cards instead of blending into the page bg when
  surface and bg are close in value (the cramped-mass look you
  flagged).
- Stronger border via color-mix with fg; subtle box-shadow for
  depth.
- Gap bumped to space-6, tile padding to space-6/7 for breathing.
Root cause of both the "all sections flash" and "intermediate scrolls
don't land" bugs was the same: the <main> element's key was bound to
safeView, which advances on every reveal in live mode. Every section
arrival changed the key, preact treated <main> as a different element,
unmounted everything and remounted — so:

- Every section gets .section-enter re-applied → the whole page
  re-animates (the flicker).
- Layout is briefly invalidated and the smooth-scroll target is
  recomputed against a momentarily empty document → the manual
  window.scrollTo I wired up last commit landed nowhere useful.

Fix: key the <main> on the time-travel pin state instead of the
live position. 'view:live' while following along (stable across
section arrivals — no remount, no flicker, scroll lands), or
'view:pin-N' when the user clicked a stepper step (intentional
remount to show that historical slice).

Same commit also clears the lingering DS gallery cosmetic bugs your
last screenshot caught:

- "FORM &AMP; SPACING" — htm doesn't decode HTML entities, so
  &amp; rendered literally then uppercased. Swapped to a middle-dot.
- Shape tile now uses the SAME pill across all four rows with only
  the radius changing — the rounding reads as a deliberate design
  choice instead of "miscellaneous samples".
- Form + spacing samples are now stacked horizontally (controls row,
  spacing row) instead of fighting for space in a side-by-side grid
  that left the spacing samples crammed in a top-right corner.
- Eyebrow text bumped to 11 px, weight 600, color via color-mix off
  the brand fg so it reads as a tile label on dark themes.
PAGE_BOOTSTRAP outline schema capped at 8 — set before the library
doubled from 14 to 24 sections. With Banner/Byline/Timeline/Callout/etc
available, real outlines hit 10–11 items routinely. Bumped to 14 (the
size of the original landing-only library) so the agent can compose
beyond-landing pages without re-architecting.

DS gallery — three sections instead of six cramped tiles
- Section 1: Hero strip (one-line: brand name + tag).
- Section 2: Color palette as a FULL-WIDTH 8-column strip with
  72 px chunky swatches. No more shoving the palette into a 1/3
  column and watching the labels truncate.
- Section 3: Typography (left) | Components (right). Typography has
  three labeled samples (display, body, caption) generously spaced.
  Components is a single column tile containing three sub-sections:
  Cards row, Buttons row, Form controls — each labeled with a small
  meta caption so the hierarchy reads, no longer a wall of
  unlabeled samples.
- Shape tile dropped — it was confusing and added little. Radius is
  implicitly visible through every other component anyway.
- Spacing strip dropped — same reason.
- 2-col layout at desktop, falls back to 1 col under 720 px.
DS gallery — root cause finally found
The reason every "increase padding" iteration looked the same: I'd been
writing var(--space-5), var(--space-7), var(--space-10) — none of which
are defined on the brand. The brand only defines space-1/2/3/4/6/8/12
/16/24. Every undefined token resolved to nothing, making the padding
declaration invalid, which CSS silently drops. The gallery has been
running with effectively zero padding the entire time.

Switched the whole DS gallery block to hard-coded pixel values
(32px/40px/48px etc.) so the layout actually breathes regardless of
what the brand's spacing scale defines. The DS gallery is iframe
chrome, not page content — it doesn't need to honor brand spacing.

Scroll — replaced rAF×2 with setTimeout(100) and added diagnostics
- rAF×2 was racing the section-in keyframe; getBoundingClientRect
  was reading the transformed position during the first frames of
  the entry animation, so the target Y was being computed against a
  partially-shifted DOM. 100ms gives layout AND the entry animation
  time to commit before we measure.
- console.debug logs the computed target so when scroll still doesn't
  land, devtools shows exactly what's going wrong (no section found
  vs. wrong math vs. browser clamping).
Two changes for the very-wide-window overflow:
- max-width on the gallery was 1280 px; tightened to 1100 px so the
  composition stays the same width regardless of viewport (~very
  wide windows just get more bg margin around the gallery instead of
  letting tiles get cavernously wide).
- The parent .page-main-ds is a flex-column with align-items: stretch
  by default, so margin:0 auto doesn't actually center the gallery
  on the cross axis. Explicit align-self: center fixes it.
- overflow: hidden on the gallery as a defensive clip so any non-
  shrinking child (long price strings, wide placeholder text in a
  card) can't push past the gallery's right edge.
…ir flush

Fundamental redesign per your spec:

1. Each rendered section carries a data-section="Name" attribute. The
   scroll function targets by that stable identifier, not by the
   .section-enter class (which animationend strips, and which can
   match the wrong element when reveals overlap).

2. scrollSectionToCenter(sectionName): setTimeout 200 ms, then call
   el.scrollIntoView({ block: 'center' }) directly. No manual math,
   no rAF×2 timing race against the entry keyframe — just the
   canonical browser API on the actual element, after layout has
   settled. console.debug logs what it does so we can verify in
   devtools if it still misbehaves.

3. revealBlock has three explicit cases:
   - A. Penultimate (last non-Footer outline item) arrives → BUFFER
        it. Don't render yet. Set 8 s safety timeout.
   - B. Footer arrives while a penultimate is buffered → flush BOTH
        into one rerender so they animate in together, then 3 s
        later scrollIframeToTop so the user lands on Hero.
   - C. Normal intermediate section → append, rerender, scroll into
        center after 200 ms.

4. .is-building padding-bottom bumped from 60 vh → 100 vh so there's
   a full viewport of slack to guarantee block:'center' can scroll
   any section to the middle, even Footer.

This is what was missing: I'd been chasing scrollIntoView timing and
math, but the real lever was waiting for the entry animation to
settle before measuring, AND identifying the target by stable data
attribute. The last build's setTimeout(100) wasn't long enough — the
section-in animation's first 50 ms or so still has translateY in
play, so getBoundingClientRect was off.
…and swap

Three follow-ups from your test run:

1. Last scroll-to-top failed. The batching path (penultimate buffer
   then Footer flush) is the happy case, but if the agent ships
   sections in an unexpected order the buffer never engages and
   Footer falls through to Case C. Case C only triggered scroll-to-
   top when the outline had NO Footer; if Footer was in outline but
   reached Case C, no scroll fired. Now Case C always checks
   blocks.length >= outline.length and scrolls to top after 3 s
   regardless of how Footer got there.

2. DS appeared to switch "boom" at the end. Two contributors:

   a. DEFAULT_PRELUDE_BRAND was colorful violet/cyan/pink — already
      looked like a brand, so when the real DS landed the swap
      didn't read as an arrival. Switched to NEUTRAL grayscale so
      any real brand pops by comparison.

   b. Body had a 320 ms transition on bg/color but every other
      primitive (.btn, .card, .input/.select/.textarea) snapped
      instantly when CSS variables changed, causing a jarring half-
      smooth half-snap effect. Added matching 320 ms transitions on
      background-color, color, and border-color across all
      brand-token-driven primitives so the whole iframe re-tints in
      one smooth pass when applyBrand fires.

If the iframe was rendering with the prelude (now neutral) brand for
the first several seconds, you'll see that clearly — and when the
real DS lands the swap is one continuous smooth color shift instead
of a snap.
Two follow-ups from your test:

1. "Rendered the section, scrolled up, then scrolled down to last
   section" after Footer. Root cause: when the agent's turn ends,
   Studio bumps refreshNonce → posts host:refresh-page to the iframe
   → the refresh-page handler was calling scrollSectionToCenter on
   the last block (Footer) as a "follow the build" gesture. This
   fought with the intentional 3 s scrollIframeToTop scheduled by
   the Footer pair-flush. Dropped the auto-scroll from refresh-page
   — it's a passive reload, not a reveal.

2. DS gallery overflowing in the 700–900 px iframe range (the
   split-pane case in your screenshot). At those widths the 2-col
   layout forced the 3-card Components row into a too-narrow
   column and bursting past the gallery's right edge.
   - Pushed the 2 → 1 col breakpoint from 720 px → 1024 px so the
     split-pane case gets the vertical-stack layout.
   - Reduced the gallery padding from 32/40 → 28/28 at wide and
     20/24 at narrow so each band has more usable width.
   - Color palette stays 8 cols above 720 px (since each tile gets
     full width in 1-col mode, 8 swatches fit cleanly).
You said: "on the very last section you didnt scroll to it, and then
it scrolled top, so i never saw the last section". That was because
both terminal paths (Case B pair flush and Case C outline-complete)
ONLY scheduled scrollIframeToTop, with no scrollSectionToCenter on
the just-landed section first. The user was scrolled wherever the
previous section had centered them, the new section landed below the
fold, and 3 s later the page jumped to top — the section never
crossed the viewport.

Fix in both cases: scroll the last section into center first (so the
user actually sees it land), THEN hold 3 s on that view, THEN scroll
to top for the Hero-first landing on the finished page.

For Case B (penultimate buffered, Footer arrives): scroll to center
the penultimate — it's the meaningful body section, and Footer
follows directly below it so the pair reads as a unit.
For Case C (outline-complete in the normal append path): scroll to
center whatever block just arrived, same as a non-terminal reveal,
then the 3 s timer fires scroll-to-top.
Cleanup pass after many feature commits accumulated layers of defensive
guards, dead diagnostics, and undefined CSS tokens. Net: −39 lines, no
behavior loss (the polish from the build choreography, the GEO export
pipeline, and the section library all preserved).

Dead code removed
- Diagnostic tips:N active:M badge in host-html.ts (was added to debug
  the "tooltips don't appear" recurrence — bug is fixed).
- onSectionAnimEnd handler + the onAnimationEnd= wiring on every
  section wrapper. The .section-enter class is gated at the source
  (seenSectionKeys), and the previous "re-flicker on remount" was
  actually the <main> remount caused by a volatile key — already fixed
  by keying on viewStepIdx. The handler was belt-and-suspenders for a
  bug that no longer exists.

Undefined-token fixes
- templates.ts had var(--space-5/10/20) usages on the new beyond-
  landing sections (MetricsGrid, Timeline, Chart, Callout, Comparison,
  BeforeAfter). The brand only defines space-1/2/3/4/6/8/12/16/24, so
  those declarations were silently dropped — sections had effectively
  ZERO vertical padding. Replaced with defined tokens.

Helper consolidation
- resetReveal() helper wraps the 5 scattered `seenSectionKeys = new
  Set()` resets at mode-change/page-swap/remove-block sites.
- pickString() + pickArray<T>() generic extractors in service.ts cut
  the verbose typecheck pyramid that was repeated dozens of times.
- Typed shape extractors (extractFAQPairs, extractHeroStats,
  extractFeatureItems, extractPricingPlans, extractTimelineEntries,
  extractMetricsEntries, extractByline, extractLongFormFirstParagraph,
  extractStatStripItems) — both buildJsonLdGraph and buildLlmsTxt now
  consume these instead of re-walking raw block.props in parallel.
  Net result: buildLlmsTxt is half the size, both functions read like
  pseudocode.

revealBlock simplification (the big one)
- Dropped the penultimate-buffer / pair-flush state machine. It was
  responsible for the recurring "last section never centered" bug
  (Cases A/B vs Case C handled scroll inconsistently) and added two
  state fields, an 8 s safety timeout, and 50+ lines of branching for
  a visual nuance (penultimate + Footer in one paint) that the user's
  actual feedback graded as MORE problems than the simpler "scroll
  every section to center" rule.
- New revealBlock: upsert, set isRunning, rerender, scroll the new
  section to vertical center. If the outline is complete, hold 3 s
  then scroll back to top.
- Removed state.pendingPenultimate + state.pendingPenultimateTimer.

.is-building plumbing dropped
- The 100 vh padding-bottom spacer was gated on state.isRunning →
  .is-building class on <main>. That coupling caused a race (late
  host:set-page-progress left isRunning=false at scroll time, no
  spacer, scroll silently clamped). Made the spacer always-on during
  page mode — it's invisible against the body bg, the post-build
  scroll-to-top puts the user above it, and one less reactive surface
  to debug.

Prompt clarification
- Added a "Disambiguation" line above the section list distinguishing
  StatStrip (social proof) vs MetricsGrid (OKR), and TestimonialQuote
  (customer voice) vs Callout (editorial note).
…ding

After the design-system gallery hold ends and before the first body
section lands, the iframe now shows the full template library — 24
sections in 8 functional categories (Structure / Pitch / Social proof /
Features / Commerce / Trust / Narrative / Data). Sections the agent
picked for THIS build are highlighted with the brand primary + a
checkmark; the rest are gray.

Communicates the "agent picked these from a library" story that was
otherwise invisible — the user only ever saw the final result of the
agent's outline choice, never the breadth it was choosing FROM.

Adaptive timing:
- Floor: 1500 ms. The library is held at least this long even when
  the agent ships RENDER_BLOCKs fast. host:render-block messages that
  arrive during the floor are queued in state.deferredRenderBlocks
  and drained as soon as the floor elapses. Result: full library
  appreciation even on a fast model.
- Ceil: 3500 ms. If no blocks arrive by then, transition to the
  layout/building phase anyway (the existing "waiting for first block"
  view). Result: no stalling on a slow model.

Implementation:
- SECTION_CATEGORIES constant (24 sections in 8 buckets, mirrors the
  agent's prompt categorization).
- SectionLibraryShowcase preact component using hard pixel padding
  (no --space-N undefined-token trap).
- .sl-* CSS block: 4-col grid → 2 → 1 across breakpoints, with
  brand-color transitions on the pills so the "selected" state
  re-tints smoothly if the user time-travels back via the stepper.
- Phase derivation extended to 'section-library' between
  'design-system' and 'layout' with libraryFloorEnds / libraryCeilEnds.
- libraryArrivedAt stamped the moment the DS hold expires.
- Stepper stays on "Design system" through the library phase (it's
  internal choreography, not a page-outline step).
- scheduleRerenderAt at both floor and ceil so the phase advances
  without external triggers.
- libraryArrivedAt + deferredRenderBlocks reset on host:prelude /
  setMode('welcome') so each new build starts clean.
Three follow-ups from the test build:

1. Library "flashed by after the first component" — root cause:
   state.libraryArrivedAt was stamped AFTER the phase derivation ran.
   On the first tick past dsHoldEnds the derivation saw null and fell
   through to layout/building; libraryArrivedAt was stamped at the
   end of the same tick. On a subsequent rerender the library briefly
   showed (within the floor window) and then disappeared. Extracted
   ensureLibraryArrivedAt() — stamps libraryArrivedAt synchronously
   anchored to dsHoldEnds — and called it BEFORE phase derivation in
   PageView AND inside the host:render-block handler so a fast first
   block is properly deferred even before any rerender.

2. Library missing from the progress topbar. Added "Library" as a real
   stepper step between "Design system" and the outline. stepperSteps
   is now [DS, Library, ...outline]; livePos = 0 / 1 / 2..N+1 maps
   onto the phases. Pinned-view (time-travel) handling updated to
   recognize step 1 as section-library.

3. Phase derivation simplified: the floor branch now triggers
   independently of blocks.length (the deferred-blocks pipeline holds
   them off-render until the floor elapses), so the library shows
   immediately on the first tick past dsHoldEnds regardless of
   whether a block message has already arrived.
…cer collapse

Three follow-ups from your test:

1. Library "flashes by too fast" — root cause was that the selected
   pills were already highlighted at frame 0, so there was no visible
   moment of "the agent picked these". Pills now START gray (same as
   unselected ones) and the highlighted state is applied via a
   pill-highlight keyframe animation with a per-pill animation-delay
   so the selections pop in one-by-one. 250ms initial settle delay +
   160ms per pill + 520ms last-pill animation. For a typical outline
   of ~10 sections that's ~2.4s of staggered reveal, which the floor
   now waits for (libraryFloor 1500→3500ms, libraryCeil 3500→5500ms).
   Respects prefers-reduced-motion.

2. "We went to the page already scrolled to the second one" — the
   deferred-block drain was firing all queued blocks in a single
   tick. Each one called revealBlock → scrollSectionToCenter on a
   200ms delay; the last one's scroll landed last and won. Now the
   drain replays blocks one at a time with a 600ms gap so the user
   sees Nav land + center, then Hero land + center, etc. — the same
   rhythm a live build would have.

3. "Footer has giant space under it after it finished the page" — the
   100vh bottom spacer was always-on (made permanent in the refactor
   pass to dodge the .is-building race). Now it has a smooth 700ms
   transition and an .is-done class that collapses it to 0 when the
   outline is structurally complete AND the agent's turn has ended
   (!isRunning). During the build the spacer carries scroll room for
   block:'center'; after the build the finished page reads naturally
   to the footer with no empty band below.
…o choreography

You were seeing a cacophony of scrolls on F5 of a finished page. Root
cause: Studio's chat-stream observer replays every historical
PAGE_RENDER_BLOCK as a host:render-block message after refresh. Each
one was hitting revealBlock, which forced state.isRunning = true and
scheduled scrollSectionToCenter — so a 10-section page produced 10
"live" reveals settling through the viewport.

Two fixes:

1. revealBlock distinguishes LIVE from REPLAY using state.isRunning
   as the truth signal. Live = task is in_progress, Studio sent
   isRunning=true via host:set-page-progress, full choreography fires
   (scroll + isRunning bump + outline-complete scroll-to-top). Replay
   = isRunning false, just commit the upsert + rerender silently. The
   forced isRunning=true line that was lying about the agent's state
   is gone.

2. host:set-page (load-page-from-disk) was already backdating
   dsArrivedAt so the DS gallery doesn't briefly flash on refresh of
   an existing page; extended it to also backdate libraryArrivedAt
   so the section-library phase is skipped too. Single combined
   "farPast" timestamp keeps the two stamps in their natural offset
   so any phase-derivation logic that compares them stays consistent.

On a refreshed done page now: the page renders directly, the stepper
hides (showStepper already gates on isRunning OR !isLive OR settle
window, none of which hold here), and no scrolls fire.
Merge design-system + section-library into a single 'design' phase
with a split-screen UnifiedDesignPhase component (DS gallery left,
section library right). Stepper collapses to ['Design', ...outline].

Add MIN_REVEAL_INTERVAL_MS=1500 queueReveal so every section gets
breathing room — drained deferred blocks and live blocks both flow
through the same paced queue.

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

When the last section (footer) lands, skip scrollSectionToCenter and
let .is-done apply the moment the outline is structurally complete
(don't wait for isRunning to flip). The footer is semantically the
bottom; centering it then collapsing the spacer caused a visible
jump. Now: footer renders at the natural bottom, brief hold, scroll
to top.

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