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 10 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" |
| ? 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/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 |
98826df to
1a388f4
Compare
b815e71 to
d86c6e0
Compare
…s, choreographed build
Adds the Page Editor: a new builtin agent that builds landing pages
section-by-section in front of the user via a real-time preview pane.
The agent emits PAGE_* tool calls (PAGE_BOOTSTRAP, PAGE_RENDER_BLOCK,
PAGE_UPDATE_BLOCK, PAGE_REMOVE_BLOCK, PAGE_REVIEW_SUGGEST, and DS
management) which Studio observes from the chat stream and dispatches
straight into an iframe as host:* postMessages — a browser-as-REPL
pipeline that skips HTTP round-trips per block for ~10× faster builds.
Main pieces:
- apps/mesh/src/tools/page-preview/ — MCP tool surface
- apps/mesh/src/page-preview/service.ts — server-side persistence
- apps/mesh/src/page-preview/templates.ts — ~24 section library
- apps/mesh/src/page-preview/host-html.ts — self-contained Preact
runtime served into the
preview iframe
- apps/mesh/src/web/layouts/main-panel-tabs/page-preview-tab.tsx
— Studio-side host + bridge
- apps/mesh/src/web/components/home/page-editor-recruit-modal.tsx
— seven-question welcome quiz
Build choreography (in host-html.ts):
- Phases: prelude → design → layout → building → done
- UnifiedDesignPhase: split-screen DS gallery + section library with
the agent's outline highlighted (staggered pill animations)
- OutlineStepper: sticky stepper with click-to-time-travel preview
- queueReveal: paced reveal queue (MIN_REVEAL_INTERVAL_MS = 1500)
so each new section gets reading room before the page scrolls
- Refresh of a done page short-circuits all choreography
Also adds:
- Contrast math (onPrimary/onSecondary/onAccent tokens) for readable
buttons on brand backgrounds
- Auto-bubble of preview runtime errors back to the agent
- GEO/SEO baseline (JSON-LD, llms.txt, robots.txt)
- WELL_KNOWN_AGENT_TEMPLATES entry + home-screen tile
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Auto-installs for every org via the existing Studio Pack pipeline, so users get the Page Editor out of the box (no recruit modal, no home-tile wiring required). - apps/mesh/src/tools/virtual/studio-pack/page-editor.ts: define pageEditorAgent with id, icon, instructions, selected tools (PAGE_*/DESIGN_SYSTEM_*), and defaultMainView=page-preview - studio-pack/index.ts: add pageEditorAgent to STUDIO_PACK_AGENTS and extend installStudioPack to honor an optional defaultMainView on agents — sets metadata.ui.layout.defaultMainView so the panel-tab resolver renders the live preview iframe - mesh-sdk constants: StudioPackAgentId.PAGE_EDITOR helper and isStudioPackAgent() prefix check - app.ts: minor biome reformat (no semantic change) New orgs install via the Better Auth org.afterCreate hook. Existing orgs pick it up on next backfillStudioPackForAllOrgs run. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rage
Fixes multi-tenant isolation and unblocks multi-instance deployment.
BEFORE: service.ts persisted pages and design systems as files under
<dataDir>/page-editor/{pages,design-systems,state.json} on the local
filesystem. getPagePreviewPaths(options) accepted an orgId argument
but ignored it — every org wrote to the same shared directory. The
service.test.ts even documented this with a "uses one well-known
local pages directory for every org" guard. Org A could read,
activate, and serve org B's pages. The setup also broke multi-pod
deployments since /data wasn't shared across instances.
AFTER: persistence flows through ctx.objectStorage (BoundObjectStorage),
which is org-bound at construction (see object-storage/factory.ts).
The org isolation is enforced by the binding — there is no orgId in
the page-preview keys at all. Keys are:
page-preview/state.json
page-preview/pages/<slug>/{index.html, app.js, sections.js, page.js, meta.json}
page-preview/design-systems/<slug>/{demo.html, demo.js, tokens.css, tokens.js, meta.json}
Files are served via Studio's canonical /api/{org}/files/{key}
redirect (api/routes/files.ts), which handles presigned URLs and
the dev-vs-prod split. The dedicated /api/{org}/page-preview/files/*
route is deleted — page-preview now reuses the same files plumbing
the rest of Studio uses.
Files changed:
- apps/mesh/src/page-preview/service.ts: replace all readFile/
writeFile/readdir/mkdir/stat with objectStorage.{get,put,list,
head,delete}. Drop getPagePreviewPaths, getServingRoots,
resolvePagePreviewAsset, insideAnyRoot, assertInside,
toRelativePath, uniquePaths, isHtmlPath, dataDirOf, readJsonSafe,
writeJson, readUtf8 — they're either replaced by I/O primitives
or were disk-only concerns. defaultBrand() function → DEFAULT_BRAND
const. PagePreviewOptions adds objectStorage and drops dataDir.
Drops the ~/deco/page-editor legacy serving root.
- apps/mesh/src/api/routes/page-preview.ts: delete GET /files/* (use
the canonical /api/{org}/files/{key} route instead). /state and
/export thread objectStorage through.
- apps/mesh/src/page-preview/host-html.ts: deriveFilesBaseFromLocation
now points at /api/<org>/files/page-preview. URL pattern drops the
redundant /files/ segment (filesBase + '/pages/...' instead of
filesBase + '/files/pages/...').
- apps/mesh/src/web/layouts/main-panel-tabs/page-preview-tab.tsx:
filesBase swapped to /api/{org}/files/page-preview.
- apps/mesh/src/tools/page-preview/index.ts: orgArgs() now returns
ctx.objectStorage. Tools fail fast with a clear error if storage
isn't provisioned. defaultBrand() → DEFAULT_BRAND.
- apps/mesh/src/page-preview/service.test.ts: rewritten against
DevObjectStorage. Adds an explicit multi-tenant isolation test
(org A's binding cannot see org B's pages).
Verified:
- bun test apps/mesh/src/page-preview/ → 26/26 pass
- bun test apps/mesh/src/tools/virtual/studio-pack.test.ts → 4/4 pass
- bun run --cwd=apps/mesh check → no new TS errors
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- B3 slug validation: introduce a SlugSchema (z.string().min(1) + at least one alphanumeric) used by every create/activate tool. Stops whitespace-only or empty slugs at the Zod boundary so the resulting validation error names the slug field rather than slugify()'s generic "Invalid slug". - B4 prop XSS sanitization: add sanitizeBlockProps() that walks an agent-supplied prop tree and collapses any href/src/-Href/-Src value with an unsafe URL scheme (javascript:, data:, vbscript:, anything outside http(s)/mailto/tel/relative/hash) down to "#". Applied at appendBlock and updateBlock in service.ts so the stored page.js export is safe. Mirrored inline in host-html.ts (as sanitizeBlockProps) so chat-stream-driven host:render-block and host:update-block dispatches are also sanitized — these don't go through the server-side write path. - B7 OTel tracing: verified defineTool already wraps every handler in ctx.tracer.startActiveSpan with success/error status. No per-handler spans needed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…2,B6,C3,C5,C6,C7)
Track B (production hardening):
- B1 cleanup on vMCP delete: COLLECTION_VIRTUAL_MCP_DELETE now prunes
the page-preview/* prefix in object storage when the deleted vMCP
is the org's Page Editor agent. Fire-and-forget so a storage hiccup
can't block the user's delete intent.
- B2 concurrent-write protection: introduces withStateLock(orgId, ...)
— a per-org in-process promise chain — and wraps every state
read-modify-write in setPagePreviewActive / setActiveDesignSystem /
refreshPagePreview / setPageProgress / createDesignSystem /
createPage / bumpRefreshVersion. Eliminates the within-pod
last-writer-wins race on state.json. Multi-pod deployments still
rely on object-storage put-per-key atomicity (which holds).
- B6 tool handler tests: 4 new tests for appendBlock / updateBlock /
removeBlock covering section-name validation, duplicate rejection,
post-Footer rejection, URL sanitization at append time, and the
update/remove round-trip on the live block list.
Track C (simplifications):
- C3 import types in page-preview-tab.tsx: drop the locally-declared
PageEntry / DesignSystemEntry / PreviewKind / PagePreviewStatus
duplicates; import from service.ts so any new field lands here
automatically.
- C5 parseJsExport helper: collapse parseTokensJsBrand +
parsePageJsBlocks into a single regex-driven parseJsExport<T>.
- C6 deriveOnColorTokens: replace the 3 identical pickReadableText
calls in normalizeBrandContrast with one inline `pick` helper.
- C7 theme catalogue: page-editor.ts INSTRUCTIONS no longer
hand-syncs the 10-row markdown theme table. Now built at module
load time from DEFAULT_THEMES. KNOWN_SECTION_NAMES exported for
any future consumer.
Also lands B5 stream-replay dedup hardening: dispatchedBlockPatches
in page-preview-tab.tsx now resets when taskId changes, so
positional-fallback ids ("#0", "#1") from a prior chat can't collide
with the new chat's positions on cross-chat navigation.
Verified:
- bun test apps/mesh/src/page-preview/ → 35/35 pass
- bun test apps/mesh/src/tools/virtual/studio-pack.test.ts → 4/4 pass
- bun run --cwd=apps/mesh check → no new TS errors
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…elper (C2) The three tool-arg readers (extractToolArgString, extractToolArgArray, extractToolArgAny) each duplicated the same priority list of arg buckets the Claude Code streaming protocol can put arguments in: record.input, record.args, record.params.arguments. Extract that list into readToolArgBuckets() and rebuild the typed readers as thin wrappers on top. extractToolArgArray now delegates to extractToolArgAny + a coercion; extractToolArgString keeps its dot-path traversal but reads buckets from the shared helper. Net: -10 lines, one source of truth for the bucket priority order. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Internal cleanups for page-preview-tab.tsx:
- C1 collapse 6 stream walkers: introduces a single walkParts(messages)
generator that yields { toolName, record, state } from every tool-call
part across the chat stream. Replaces the same nested
`for-of messages → for-of parts → partToolName → record/state lift`
pattern that was repeated in deriveSessionItems, hasAnyPreviewToolFired,
deriveLiveOutline, deriveReviewTips, deriveLiveProgress, and
deriveBlockPatchCalls. One iteration shape, one place to change.
- C4 extract useIframePostMessageBridge: lifts the ~120-line iframe
message listener effect out of PagePreviewTab into a co-located hook
that takes typed callback props for each event (host-ready,
prompt, prompt-and-send, select-ds, close-ds-grid, request-refresh,
runtime-error). The hook is the sole owner of the
addEventListener/removeEventListener pair — lifecycle visible in
one place, component body focused on intent dispatch + render.
Dev experience:
- Enable iframe auto-reload (effective HMR for the preview): the
/host route in dev mode injects a tiny ~5-line poller that hits a
new /host-version endpoint every 1.5s. The endpoint returns a
sha1 prefix of PAGE_PREVIEW_HOST_HTML — bun --hot reloads the
host-html module on every save, the export changes, the hash
changes, the iframe location.reload()s. State is recovered the
same way it is on first mount: Studio's tab.tsx re-dispatches
intent via host:hello / host:set-page after the iframe handshake.
Production gets the raw HTML (no poller, no extra requests).
Verified:
- bun test apps/mesh/src/page-preview/ → 35/35 pass
- bun run --cwd=apps/mesh check → no new TS errors
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Every major product feature now gets a co-located harness: features/<name>/feature.md # story, value, happy path, prompt for AI agents features/<name>/happy-path.test.ts # executable contract The CLI at scripts/features.ts (wired as features:list / features:test / features:new) lists and runs the catalog. AGENTS.md teaches the contract: before touching code in a documented area, run the harness, extend the test for the new behavior FIRST (RED), implement until GREEN, only then ask a human to verify. "Studio needs to make itself." This PR ships the spine + the first concrete entry: - features/README.md — catalog invariants - features/page-editor/feature.md — the Page Editor story end-to-end, including the happy path, the file list of what implements it, the Maintenance loop, and a prompt for AI agents extending the feature. - features/page-editor/happy-path.test.ts — 9 active tests across 5 phases (Studio Pack contract, drive the agent's tool sequence, assert server state, multi-tenant isolation, export bundle). Phase F (browser via Playwright, gated by PW=1) is reserved as describe.skip for a follow-up. - scripts/features.ts — thin CLI that locates features and forwards to `bun test`. Exits non-zero when a catalogued feature lacks a test. - package.json — wire features:list / features:test / features:new. - AGENTS.md — new "Working on a feature (the harness contract)" section immediately after Overview, plus a pointer in Testing Guidelines. - .gitignore — add .agents/ so Conductor stops flagging it. - apps/mesh/scripts/test-page-preview-mcp.ts — deleted; superseded by the harness (its assertions are now Phase E + a much larger contract). Verified end-to-end: - `bun run features:list` → lists `page-editor` with tagline + test-present indicator. - `bun run features:test page-editor` → 9 active pass, 1 skip (Phase F). ~150 ms. - Deliberately remove `defaultMainView` from pageEditorAgent → Phase A fails on the exact contract line; restore → green. - `bun test apps/mesh/src/page-preview/` still 35/35 pass (no regression on existing unit suite). - `bun run --cwd=apps/mesh check` no new TS errors. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ation Two changes that flesh out the feature catalog beyond the data-path contract that shipped last commit. Phase F (deterministic, in CI): - New apps/mesh/e2e/tests/features/page-editor.browser.spec.ts uses Playwright + the existing apps/mesh/e2e/fixtures/auth.ts signup helper. After signup, the spec navigates to /<org>/agents, waits for the Page Editor card to appear (Studio Pack install is async via a DBOS workflow — generous timeout absorbs the race), clicks into the agent, asserts the URL carries `virtualmcpid=studio-page-editor_*`, and confirms the iframe titled "Page preview" loads with the preact bundle actually executing through to the welcome state. - scripts/features.ts now runs both legs: the in-process Bun tests always run; when PW=1, a matching `*.browser.spec.ts` next to the e2e fixtures is also executed via playwright. The dev server auto-starts via the existing playwright.config webServer. - features/page-editor/happy-path.test.ts loses the Phase F skip placeholder (the real spec is the source of truth now). Webwright (exploratory, before the human pass): - Each feature.md now has an "Exploratory verification" section describing the happy path as a task for Microsoft Research's Webwright (https://github.com/microsoft/Webwright) — an MIT-licensed browser-agent framework that turns a plain-English task into a re-runnable Playwright script + screenshots + a numbered-CP self-verification log. Install via `/plugin install webwright@webwright` as a Claude Code skill. - features/README.md explains the two-layer model: deterministic spec is the gate (CI), Webwright is the canary (an LLM stress-tests the happy path against the live UI before a human verifies). Output is evidence for the human, never a substitute for the deterministic contract. - AGENTS.md gains a brief pointer to Webwright in the "Working on a feature" section. Verified: - bun run features:test page-editor → 9/9 pass (~150 ms). - bun run --cwd=apps/mesh check → no new TS errors. - bun run fmt clean. PW=1 path is exercised against a running dev server; CI nightly job to actually invoke it is Phase 2. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two pieces of churn from main: - studio-pack/types.ts dropped BuildWelcomeMessage / WelcomeContext — the welcome message field is no longer part of the contract. Drop the import + the inline welcomeMessage definition; the agent gets its first impression from the chat composer's empty state, same as the other Studio Pack agents (Agent Manager, Connection Manager, Automation Manager). - Every other Studio Pack agent now declares a `selectedPrompts` array; the install loop reads it as `agent.selectedPrompts`. Add the empty array so the TS union narrows correctly. Verified: - bun run --cwd=apps/mesh check → no TS errors in page-preview / studio-pack. - bun test apps/mesh/src/page-preview/ + studio-pack.test.ts → 39/39 pass. - bun run features:test page-editor → 9/9 pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
d86c6e0 to
a0d01e9
Compare
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
Ships a local‑first Page Editor with design systems, live preview, time‑travel, and zero‑build export — auto‑installed as a Studio Pack default that opens in the new Page Preview tab. Adds a self‑verifying feature catalog and CLI for the Page Editor, now with a browser e2e leg and Webwright tasks.
New Features
PAGE_BOOTSTRAP,PAGE_RENDER_BLOCK,PAGE_UPDATE_BLOCK,PAGE_REMOVE_BLOCK,PAGE_REVIEW_SUGGESTstream to an in‑iframe Preact host via postMessage for ~10× faster builds. Includes a split‑screen design‑system gallery, 24‑section library, time‑travel, and a paced reveal queue (honors reduced‑motion)./api/:org/page-preview/*routes, including a framablehostshell (X‑Frame‑OptionsSAMEORIGIN), a dev‑mode auto‑reload hook via/api/:org/page-preview/host-version, and export endpoints.fflate, curated default themes, and WCAG contrast enforcement with derivedonPrimary/onSecondary/onAccent. Adds JSON‑LD +llms.txt+robots.txtfor crawlers.features/harness withfeature.md+happy-path.test.tsand CLI (features:list|test|new); Page Editor registered with a full happy‑path test. Adds a Playwright browser leg (PW=1runs both legs) that verifies the Page Preview iframe boots, plus Webwright tasks and AGENTS.md guidance.Bug Fixes
/api/:org/files/page-preview/*), fixing cross‑org bleed and enabling multi‑instance deploys. Also prunespage-preview/*objects when the Page Editor vMCP is deleted.href/srcprops to allow only http(s)/mailto/tel/relative/hash; reject empty or whitespace slugs with a strictSlugSchema.walkPartsiterator; iframe postMessage bridge extracted to a hook and gains dev auto‑reload; chat input bridge enables prompt composition from the preview. Unit tests cover contrast/service/tool handlers via the feature harness.Written for commit a0d01e9. Summary will update on new commits.
Review in cubic