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