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 24 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/page-preview/host-html.ts">
<violation number="1" location="apps/mesh/src/page-preview/host-html.ts:203">
P2: New thinking-state animations ignore `prefers-reduced-motion`; include these classes in the reduced-motion override to avoid continuous motion for users who request reduced animation.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| box-shadow: | ||
| 0 0 60px color-mix(in srgb, var(--brand-primary) 35%, transparent), | ||
| 0 0 140px color-mix(in srgb, var(--brand-primary) 18%, transparent); | ||
| animation: orb-spin 6s linear infinite, orb-breathe 3.4s ease-in-out infinite; |
There was a problem hiding this comment.
P2: New thinking-state animations ignore prefers-reduced-motion; include these classes in the reduced-motion override to avoid continuous motion for users who request reduced animation.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/page-preview/host-html.ts, line 203:
<comment>New thinking-state animations ignore `prefers-reduced-motion`; include these classes in the reduced-motion override to avoid continuous motion for users who request reduced animation.</comment>
<file context>
@@ -173,6 +173,93 @@ export const PAGE_PREVIEW_HOST_HTML = `<!doctype html>
+ box-shadow:
+ 0 0 60px color-mix(in srgb, var(--brand-primary) 35%, transparent),
+ 0 0 140px color-mix(in srgb, var(--brand-primary) 18%, transparent);
+ animation: orb-spin 6s linear infinite, orb-breathe 3.4s ease-in-out infinite;
+ }
+ @keyframes orb-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
</file context>
Tip: Review your code locally with the cubic CLI to iterate faster.
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 |
… 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>
1128b36 to
aa7622e
Compare
Expand the welcome quiz from 3 required questions (kind / vibe / audience)
+ one trailing textarea to 7 fully-optional questions, each with cards
and an optional free-text "details" slot.
Synthesizes a competing-perspectives review (conversion fundamentals vs.
brand specificity / anti-slop):
1. I'M BUILDING — page kind (no free-text)
2. THE PRODUCT IS — product pattern + proper-noun free-text
3. WRITTEN FOR — audience + "what they're replacing" free-text
4. ONE GOAL — CTA pattern + exact-button-label free-text
5. VOICE LIKE — named-brand voice ref + free-text link/quote
6. VISUAL ANCHOR — wired to system-prompt design-language anchors
7. PROOF I HAVE — proof type + verbatim free-text, with explicit
"Nothing yet — do not fabricate" option
Voice is split off from visual: voice is what fights generic copy
(named-brand reference >> adjective vibes), visual is what fights
generic CSS. Color is folded into visual anchor.
Every detail field wraps the user's text as "use VERBATIM, do not
paraphrase, do not invent additions" so Claude treats it as ground
truth rather than hint. The PROOF question's "Nothing yet" option
explicitly tells the agent to skip LogoStrip/TestimonialQuote rather
than fabricate "Trusted by 10,000+ teams" social proof.
All questions optional — the Generate prompt button is always live.
Composer emits only the lines for questions actually answered, and
defaults to a minimal "Build a landing page." stub if nothing was
picked. Clicking a selected card a second time clears it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ilding The outline stepper at the top of the preview pane becomes a scrub bar: clicking a done or current step pins the right pane to that historical snapshot while the agent keeps working in the background. A "Live ›" pill appears when pinned, restoring the live follow. Concretely: - A "Design system" step is prepended to the stepper (index 0). Clicking it rewinds the preview to the InlineDSGallery (colors, type, buttons, spacing) even after sections have started landing — so the user can re-admire the DS while the rest of the page assembles. - Each subsequent step (Nav, Hero, Features, …) corresponds to "first N sections rendered". Click a done step and the right pane crossfades back to that state. - Planned steps (ahead of the live cursor) stay inert. Can't time-travel to a future that hasn't happened. - DraftingSlot (pulsing "Adding next…" placeholder) only renders in live mode — hidden when pinned to a past snapshot so the snapshot looks clean. - <main> is keyed on the resolved view step, so a step click remounts the element and re-triggers a 280ms fade-in. - viewStepIdx clears on mode transitions so a pin from one chat doesn't bleed into the next. CSS adds a new .toc-item-viewing state (inverted fg/bg pill so the viewed step pops), .toc-item-clickable (cursor pointer + lift-on-hover), .toc-item-rewindable (a small ↻ glyph hint on past clickable steps), and the .toc-item-live "Live ›" pill that pulses while pinned. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When the preview iframe catches a runtime error (SyntaxError, broken section, undefined identifier, etc.) and the chat task is idle, send the structured error report directly via chatStream.sendMessage() instead of just composing it into the input. The agent picks it up like any other prompt and starts fixing immediately — no user click required. Gates / dedupe: - Only auto-bubble when the agent's turn has finished. While the task is in_progress or expired we don't pile on another user message; let the current turn finish first. (The in-iframe error card still shows, so the user can manually escalate via "Ask the agent" if they want to interrupt.) - A dedupe ref (lastAutoReportedErrorRef) holds the signature of the most recently auto-reported error (headline + location + first 240 chars of message). Identical errors that fire again before the agent has had a turn do not re-send. - The dedupe clears the moment the chat task transitions back to idle. So if the agent's fix didn't work and the next refresh re- errors with the same signature, the auto-bubble re-fires. Loops on the same error only happen because the agent keeps trying and failing — user can always interrupt. - Fallback to composeChatInput() when there's no active chat stream (no agent task open) or auto-bubble was skipped — preserves the manual "Ask the agent" flow. Live values (stream + taskStatus) reach the window message listener via mutable refs assigned during render, since the listener is registered once on mount and can't read live React state directly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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 a single host‑iframe live preview, step‑by‑step progress, an interactive time‑travel stepper, and zero‑build export. Pages and design systems are split; you can retheme or switch pages without reloads, and the welcome quiz is now a 7‑question optional flow that composes a grounded prompt. Preview runtime errors now auto‑report to the agent when idle for faster fixes.
New Features
/api/:org/page-preview/hostdriven by postMessage (welcome, set/refresh page, retheme, inline design‑system gallery/grid) with crossfades, a thinking intermission, section reveals + auto‑scroll, and a progress overlay + outline stepper viaPAGE_PREVIEW_PROGRESS— the stepper is now clickable to time‑travel between steps, with a “Live ›” pill to return to live; a “Design system” step is prepended and future steps are inert.export const PAGE = []with a richer sections library; only new blocks animate in.DESIGN_SYSTEM_CREATE/LIST/SET,PAGE_PREVIEW_PAGE_CREATE{activate?},PAGE_PREVIEW_STATUS/SET/REFRESH,PAGE_PREVIEW_PROGRESS; next‑step hints guide the agent through build steps.fflate) that inlines CSS, hoists a singlepreact/htmimport, synthesizes aSectionsnamespace, and preserves sources undersrc/.Bug Fixes
/page-preview/filesand/page-preview/host.SELFand dev‑assets pseudo‑connections handled in‑process; SWR bypass so new tools appear immediately.DESIGN_SYSTEM_CREATE/PAGE_PREVIEW_PAGE_CREATEand updates during inline DS gallery; thinking intermission holds until real content; fallback “Working…” pill between labels.Written for commit 98826df. Summary will update on new commits. Review in cubic