Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion docs/features/agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -497,7 +497,7 @@ The agent works **design-system-first**: it establishes or reuses tokens, then r

| Tool | Input | Success `data` | What it does |
|-------------------|-----------------------|----------------|------------------------------------------------------------------|
| `site_render_snapshot` | `{ breakpointId?, nodeId? }` | `{ breakpointId, nodeId?, label, width, capturedAt, layout, screenshot }` + optional `images[]` | Inspect the rendered canvas: always returns a layout report (viewport, per-node bounding boxes, overflow / broken-image / invisible warnings); when the active provider supports native image-bearing tool results, a PNG is attached via the tool-output **image channel**. `breakpointId` picks the frame (defaults to active); `nodeId` scopes the capture to that node's subtree — image and report cover only that section, with coordinates relative to its box, and the report carries the same `nodeId`. Omit `nodeId` for the whole page; an unknown `nodeId` returns an `aiToolError` |
| `site_render_snapshot` | `{ breakpointId?, nodeId? }` | `{ breakpointId, nodeId?, label, width, capturedAt, layout, screenshot }` + optional `images[]` | Inspect the rendered canvas: always returns a layout report (viewport, per-node bounding boxes, overflow / broken-image / invisible warnings); when the active provider supports native image-bearing tool results, a PNG is attached via the tool-output **image channel**. `breakpointId` picks any configured viewport at its exact configured width (defaults to active). Evidence always renders through a deterministic one-shot frame, so Live mode, collapsed frames, and `previewFrame:false` do not change the result or the user's visible canvas state. `nodeId` crops the rendered document to a smaller, sharper model image while preserving HTML/body/ancestor paint; the report covers the same section with coordinates relative to its box. Unknown breakpoint/node ids return `aiToolError` |

### Auto-navigation

Expand Down Expand Up @@ -550,6 +550,10 @@ The content system prompt is markdown-native: it tells the model to exchange bod
4. **`site_read_document` is cleaned and paged before it reaches the model.** `renderAgentDocument` strips pathological strings from the broad read surface: long base64/data URLs become `data:<mime>;base64,[omitted N chars]`, and very long URLs are middle-truncated. The returned object always includes `pageInfo` with `part`, `totalParts`, `nextPart`, `ranges`, `serializedChars`, and cleanup counts. The hard budget is measured against `JSON.stringify({ html, css, pageInfo }).length`, because that is the text providers receive as the tool result. If `nextPart` is not `null`, the agent calls `site_read_document({ document, part: nextPart })` to continue. For exact node-level markup, use the `uid` with `site_get_node_html`.
5. **Stale evidence is elided.** Within one tool loop, only the **most recent** heavy result per tool name (`site_render_snapshot`, `site_read_document`, `site_get_node_html`, or anything with an image) is replayed at full fidelity; earlier ones are rewritten to a one-line breadcrumb (`"Earlier <tool> output removed… Call <tool> again…"`). Older snapshots describe page state the model has since mutated, so they carry no value. See `applyHeavyElision` in `server/ai/drivers/http/toolLoop.ts`.

Snapshot pixels come from the iframe document's authored rendering. Full-page captures rasterise `<html>` at the exact iframe viewport width and full document height, so a narrow/transformed body cannot shrink the reported viewport or omit document gutters. Node-scoped captures rasterise that same document painting context and crop to the node rectangle, so transparent sections retain HTML/body/ancestor colors, gradients, and background images. A white browser-default fallback is composited only behind pixels the authored page leaves transparent — it is never written onto the cloned document.

Every request uses `AgentSnapshotFrame`, an offscreen one-shot `IframeFrameSurface` at the configured width. Before it becomes capturable, a revisioned barrier waits for template preview rows, nested loop data, media metadata, web fonts, the resulting React commit, and a quiet DOM window. Readiness lives on the host iframe, never on authored `<html>`/`<body>`, so user attribute selectors cannot distinguish Agent evidence from the published page. Lazy `<img>` resources are left authored as-is; `html-to-image` makes its private clones eager and embeds image/background resources before `toCanvas()` resolves. The frame deliberately does not execute authored runtime scripts, and it is released after capture without changing `activeBreakpointId`, `canvasView`, or collapsed-frame state. Parallel requests are serialized so they cannot replace the single transient frame mid-capture.

---

## System prompt
Expand Down
16 changes: 15 additions & 1 deletion docs/features/canvas-iframe-per-frame.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ Each viewport frame runs in its own `<iframe>` with its own `<html><body>`. The

## TL;DR

- `IframeFrameSurface` is the iframe primitive. It boots from an empty `srcDoc`, captures the iframe document, and mounts children via `createPortal(tree, iframeDoc.body)`.
- `IframeFrameSurface` is the iframe primitive. It boots from a sentinel-tagged empty `srcDoc`, ignores the browser's short-lived initial `about:blank` document, and mounts children once via `createPortal(tree, iframeDoc.body)` only after the final document is ready. `CanvasDocumentContext` exposes that document to body ownership/evidence helpers without inserting probe elements into authored DOM.
- **Design mode** renders one `IframeFrameSurface` per framed viewport context inside `CanvasTransformLayer` (pan/zoom). All frames mount as soon as the page document is in the store — the tree is already in memory, so there is nothing to stagger; `CanvasTransformLayer` renders skeleton frames only while the document itself hasn't loaded yet (`page === null`). **Live mode** renders a single real-size `IframeFrameSurface` inside `CanvasLiveSurface` (normal scroll).
- Both modes are fully editable — click-to-select, properties panel, structural edits all work. Neither is a read-only preview.
- Agent evidence can request any configured viewport. Each capture renders once through an offscreen `AgentSnapshotFrame` at the configured width, then removes it without changing the visible canvas state.
- CSS arrives in each iframe via three injectors: `EditorChromeInjector` (unlayered), `ClassStyleInjector` (`@layer user-authored`), `UserStylesheetInjector` (`@layer user-authored`).
- Wheel, pointer, and keyboard events are forwarded from inside the iframe to the parent's gesture / reorder-drag / shortcut handlers. `Tab` is blocked to prevent tab-walking inside the design preview.
- Plugin canvas modules use a separate, sandboxed `ModuleSandboxFrame` (not `IframeFrameSurface`).
Expand Down Expand Up @@ -74,6 +75,18 @@ Frames mount as soon as the page document is in the store. The node tree is alre

The active viewport context (highlighted, drives style override routing) is tracked by `activeBreakpointId` in `canvasSlice`.

### Agent evidence frames

`site_render_snapshot` always asks `CanvasRoot` to mount one `AgentSnapshotFrame` through a portal outside the canvas clipping/transform layers. It uses the same `IframeFrameSurface`, `CanvasComposedTree`, breakpoint context, class CSS, and user stylesheet path as the editor, at the breakpoint's configured width. A deterministic transient path avoids capturing a visible frame midway through an asynchronous preview fetch and makes Live/collapsed/disabled-frame state irrelevant. The frame sits offscreen rather than using `display:none`, so it has real layout geometry for evidence collection.

This frame is transient editor-session state. It does not change the active viewport, Design/Live mode, pan/zoom, or collapsed frame ids, and it never runs authored runtime scripts (a read must not duplicate arbitrary side effects or network calls). A revisioned readiness tracker covers post-type preview rows, loop data, and media metadata; capture additionally waits for fonts, React/DOM settling, and cloned image/background embedding. The request-specific marker is stored on the host iframe rather than authored DOM. The executor captures only after that marker appears and releases the request in `finally`. Explicit breakpoint lookup is exact; it never substitutes another frame.

### Body presentation and snapshot paint

The iframe's real `<body>` owns the root node's classes, inline styles, and sanitized authored HTML attributes, matching the publisher's `<body class="…" style="…" data-…>`. `applyIframeBodyPresentation` is shared by the editable `base.body` component and template composition, where the outermost wrapper body owns the published body. Both consume `CanvasDocumentContext` and render no editor-only probe child, so `body > :first-child`, `:nth-child`, sibling, and `:empty` selectors see the published structure. The helper restores only the declarations/attributes it touched; design-frame height/overflow declarations remain owned by the grow-to-content reset, while Live keeps authored body sizing exactly as published.

Agent screenshots preserve the complete document paint. A full capture rasterises `<html>` at the exact iframe viewport width and full document height; a node-scoped capture rasterises the same HTML/body/ancestor context and crops it to the target rectangle instead of detaching the transparent subtree. The raster fallback is composited behind the finished page pixels, so it cannot overwrite authored colors, gradients, or background images.

**Initial centering on load and document switch.** The transform layer always starts at pan `(0, 0)`, which places the leftmost (narrowest) frame at the top-left. On first load and whenever the active document changes (page switch, entering/leaving a Visual Component), `CanvasRoot` runs a `useEffect` keyed on `canvasPage.id` that calls `useCanvas().centerOnBreakpointFrame` to pan the canvas so the active breakpoint frame is horizontally centered and its top sits just below the viewport top. The geometry is computed by `panToCenterBreakpointFrame` in `canvasDomGeometry.ts`. The effect retries on a short `setTimeout` loop (not `requestAnimationFrame`, which is skipped for backgrounded tabs) until the iframe-backed frames have real layout geometry. The current zoom is preserved; only the pan changes. Breakpoint switches within the same document (toolbar, node clicks) do not re-center — the designer keeps their place. See [docs/features/editor-preferences.md](editor-preferences.md) for how the `defaultBreakpoint` preference plugs into this.

### Viewport Activation UX
Expand Down Expand Up @@ -198,6 +211,7 @@ Tests that render the canvas and query nodes must use the `iframeCanvasQuery.ts`
- Source-of-truth files:
- `src/admin/pages/site/canvas/IframeFrameSurface.tsx` — iframe primitive
- `src/admin/pages/site/canvas/CanvasLiveSurface.tsx` — live mode surface
- `src/admin/pages/site/canvas/AgentSnapshotFrame.tsx` — one-shot offscreen frame for exact Agent breakpoint evidence
- `src/admin/pages/site/canvas/BreakpointFrame.tsx` — design mode per-viewport frame
- `src/admin/pages/site/canvas/CanvasTransformLayer.tsx` — design mode pan/zoom container; renders all frames once the document loads, skeleton frames while it hasn't
- `src/admin/shared/CanvasFrameSkeleton/CanvasFrameSkeleton.tsx` — shared frame skeleton for the document-loading / startup states
Expand Down
2 changes: 1 addition & 1 deletion server/ai/tools/site/writeTools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,7 @@ const renderSnapshotTool: AiTool = {
scope: 'site',
execution: 'browser',
description:
"Inspect the rendered canvas. Returns a layout report: viewport size, per-node bounding boxes, image-load status, and warnings (overflow / broken-image / invisible-node) — enough to catch most layout bugs in text. When the active provider supports native image-bearing tool results, a screenshot is also attached as an image. Pass `breakpointId` to choose which breakpoint frame (defaults to active). Pass `nodeId` to capture just that node's subtree — a sharper, cheaper image than the whole page, and a report scoped to that section with coordinates relative to the node; omit `nodeId` to capture the full page.",
"Inspect the rendered canvas. Returns a layout report: viewport size, per-node bounding boxes, image-load status, and warnings (overflow / broken-image / invisible-node) — enough to catch most layout bugs in text. When the active provider supports native image-bearing tool results, a screenshot is also attached as an image. Pass any configured `breakpointId` to render a readiness-aware one-shot frame at that exact configured width, independent of collapsed/disabled frames or the viewport active in Live mode (defaults to active; unknown ids error). Pass `nodeId` to crop the document rendering to that node while preserving its HTML/body/ancestor background — a smaller, sharper model image with a report scoped to that section and coordinates relative to the node; omit `nodeId` to capture the full page.",
inputSchema: RenderSnapshotInputSchema,
}

Expand Down
Loading
Loading