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
26 changes: 17 additions & 9 deletions docs/features/agent.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/reference/architecture-tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ See [docs/features/plugin-system.md](../features/plugin-system.md).

| Test | What it enforces |
|-----------------------------------------------|----------------------------------------------------------------------------------|
| `ai-tool-input-object.test.ts` | Every provider-facing site/content tool schema has a plain `type: object` root with no root-level `anyOf`, `oneOf`, or `allOf`, matching Anthropic's tool-schema contract while keeping one cross-provider registry. |
| `ai-tool-schema-ssot.test.ts` | Site write-tool input schemas live once in `src/core/ai/toolSchemas.ts` (re-exported from `@core/ai`). Every registered tool's `inputSchema` is the exact object from `@core/ai` (referential identity check); consumer modules (`writeTools.ts`, `executor.ts`, `tokenRunners.ts`) import from `@core/ai` and do not redeclare local copies. |
| `ai-driver-isolation.test.ts` | Provider SDKs (`@anthropic-ai/claude-agent-sdk`, `@openai/agents`, `@openrouter/agent`), `zod`, and `@anthropic-ai/sdk` are banned repo-wide with no allowed callers. `@modelcontextprotocol/sdk` is **scoped**: allowed only under `server/ai/mcp/` (the MCP server), banned everywhere else (drivers + browser). Drivers talk directly to each provider's REST API over HTTP/SSE and pass TypeBox schemas through as JSON Schema. `src/` and `server/` are both scanned. |
| `ai-mcp-connectors-never-leak.test.ts` | The MCP connector wire projection (`toConnectorView`) and `McpConnectorViewSchema` never expose the token or its hash. Mirrors `ai-credentials-never-leak.test.ts`. |
Expand Down
26 changes: 16 additions & 10 deletions docs/reference/css-class-registry.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

The site's style rule registry — `Record<string, StyleRule>` stored on the site shell (`site.styleRules`). Every user-defined CSS rule lives here. The publisher compiles entries to CSS at publish time; the editor's canvas injects the same CSS for live preview.

Two kinds of rules:
Three forms of rules:

1. **Author-facing class rules** (`kind: 'class'`) — the user picks a name (`hero-button`, `card-meta`) and the editor applies them via `node.classIds`. Selector is `.<name>`.
2. **Ambient rules** (`kind: 'ambient'`) — attach by CSS selector matching, not by node assignment (e.g. `h1`, `.hero .title`, `a:hover`). The publisher emits the rule but never writes to `class=` attributes. Supported stylesheet-level imports such as `@keyframes` are stored as ambient rules with `rawCss`.
Expand Down Expand Up @@ -39,12 +39,14 @@ interface StyleRule {
nodeId: string
role: 'module-style'
}
styles: Record<string, unknown> // base CSS properties (CSSPropertyBag-shaped at write time)
contextStyles: Record<string, Record<string, unknown>> // per-context overrides, keyed by context id
rawCss?: string // supported raw at-rule CSS, currently imported @keyframes
generated?: GeneratedClassMetadata // framework-generated flags
createdAt?: number
updatedAt?: number
styles: Record<string, unknown> // base CSS properties (CSSPropertyBag-shaped at write time)
stylePriorities?: Record<string, 'important'>
contextStyles: Record<string, Record<string, unknown>>
contextStylePriorities?: Record<string, Record<string, 'important'>>
rawCss?: string // supported raw at-rule CSS, currently imported @keyframes
generated?: GeneratedClassMetadata // framework-generated flags
createdAt?: number
updatedAt?: number
}
```

Expand All @@ -57,6 +59,8 @@ interface StyleRule {

`styles` and `contextStyles` are typed `Record<string, unknown>` at the persistence boundary — narrowing happens at the publisher's `bagToCSS` (`classCss.ts`). The WRITE API (class slice, framework generators) uses the typed `CSSPropertyBag` shape from `src/core/page-tree/cssPropertyBag.ts`.

Declaration priority is stored structurally, beside the scalar property value. `stylePriorities` is a sparse property-to-`'important'` map for `styles`; `contextStylePriorities` is the equivalent sparse map keyed first by context id. The CSS importer reads priority through `CSSStyleDeclaration.getPropertyPriority()`, and the publisher appends ` !important` from this metadata. Removing a value also removes its priority entry, and tolerant persistence parsing drops orphaned or invalid priority metadata. Node inline styles deliberately keep their existing value-only shape.

`rawCss` is intentionally narrow. The importer uses it for sanitised `@keyframes` blocks that cannot be represented as selector declarations; the publisher emits only supported raw keyframes after its own safety gate. General arbitrary CSS strings still belong in structured `styles` / `contextStyles` entries.

---
Expand Down Expand Up @@ -172,7 +176,7 @@ All usage logic lives in `src/admin/pages/site/panels/selectorUsage.ts`.
```text
For each rule in registry (sorted by order):
selector = rule.selector // e.g. '.hero-button' or 'h1 > span'
base CSS = bagToCSS(rule.styles)
base CSS = bagToCSS(rule.styles, options, rule.stylePriorities)
emit: '${selector} { ${base CSS} }'

for each (contextId, bag) in rule.contextStyles:
Expand All @@ -182,7 +186,8 @@ For each rule in registry (sorted by order):
prelude = '@media ${breakpoint.mediaQuery ?? `(max-width: ${width}px)`}'
else: // orphaned key — skipped
continue
emit: '${prelude} { ${selector} { ${bagToCSS(bag)} } }'
priorities = rule.contextStylePriorities?.[contextId]
emit: '${prelude} { ${selector} { ${bagToCSS(bag, options, priorities)} } }'
```

Cascade order within a rule: base → custom conditions (registry order) → viewport contexts. Pure max-width contexts emit widest first so narrower queries win; pure min-width contexts emit narrowest first so wider queries win; mixed/custom viewport queries keep registry order.
Expand All @@ -194,11 +199,12 @@ The compiled string is part of the per-page CSS bundle (see [docs/features/publi
Translates the property bag (`{ color: '#fff', padding: { top: 16, right: 8 } }`) to CSS strings. Handles:

- Plain values: `color: #fff;`
- Sparse priority metadata: `color: #fff !important;`
- Spacing bags: `padding: 16px 8px 0 0;` (decomposed)
- Variable references: `color: var(--site-primary);`
- Multi-value props (transforms, transitions): joined per CSS rules

Invalid entries are silently dropped — the bag is tolerant.
Invalid entries are silently dropped — the bag is tolerant. Four stored padding or margin sides collapse to a shorthand only when all four priorities match; mixed priorities remain longhands so the cascade is preserved.

---

Expand Down
5 changes: 3 additions & 2 deletions server/ai/tools/site/systemPrompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,9 @@ Design system first:
Structure as HTML, styling as CSS:
- Structure goes in site_insert_html/site_replace_node_html as semantic HTML. Style it with CSS in the SAME call: a <style> block and/or class= attributes (the importer turns these into reusable classes + ambient rules), referencing the design tokens above. This is the clean default; do NOT hand-build classes node-by-node.
- Inline style= attributes also work: they land on the node's inline styles. Fine for one-off tweaks; reach for a <style> class when a style repeats.
- site_apply_css is the ONE tool for authoring or editing CSS on its own — after insertion, or for any selector a class= can't express. Pass real CSS text: a bare \`.foo { … }\` selector creates/edits a reusable class; ANY other selector (\`.hero a\`, \`a:hover\`, \`nav > li\`, \`.card::before\`, \`h1\`) creates/edits an ambient rule that attaches by matching. Re-applying a selector MERGES onto it, so site_apply_css both creates AND edits — that is how you restyle an existing descendant/pseudo rule (e.g. \`site_apply_css(".hero a:hover { color: var(--primary) }")\`). There is no class-by-id patch tool; just write the CSS, referencing tokens via var(--…).
- site_apply_css is the ONE tool for CSS on its own. Its required operation is explicit: merge + css for normal additive edits; remove-properties + exact selectors/property names to clear stale declarations; replace + css only when you are supplying each selector's COMPLETE desired base/responsive CSS; delete + exact selectors to remove whole rules. Before replace/delete/remove-properties, call site_read_document and copy the full selector exactly — \`.grad\`, \`.hero .grad\`, and \`.grad, .hero .grad\` are distinct rules. A bare \`.foo { … }\` is a reusable class; descendant/pseudo/element/grouped selectors are ambient rules. CSS priorities such as !important are preserved, but use them only when the cascade genuinely requires them.
- Per-breakpoint variation: use @media queries — in the <style> block of an insert, or inside site_apply_css — with min/max-width queries that line up with the breakpoint widths in the dynamic suffix. Don't invent "mobile"/"tablet"/"desktop".
- For visual/cascade debugging, scope site_render_snapshot to the affected node and inspect layout.nodes[].computed (including background image/clip and WebKit text fill); pair that computed evidence with site_read_document's source CSS before editing.

Behavior and runtime code:
- site_insert_html/site_replace_node_html deliberately strip <script> and inline event handlers (onclick/onload/etc). NEVER try to add behavior with <script>, onclick, or custom inline JS in HTML.
Expand Down Expand Up @@ -68,7 +69,7 @@ Templates (CMS layouts):

Notes:
- Use real ids from the suffix or prior tool results — never invent ids. Class refs accept id OR name.
- Browser write-tool success data uses explicit keys: cssRulesCreated/cssRulesUpdated for site_apply_css, pageId for site_add_page/site_duplicate_page, nodeId/nodeIds for site_duplicate_node, and nodeIds for HTML inserts.
- Browser write-tool success data uses explicit keys: cssRulesCreated/cssRulesUpdated/cssRulesDeleted/cssPropertiesRemoved for site_apply_css, pageId for site_add_page/site_duplicate_page, nodeId/nodeIds for site_duplicate_node, and nodeIds for HTML inserts.
- On tool error: read the message and retry with corrected input.

Reply: 1-2 sentences after acting. No raw HTML/CSS/JSON in the reply — tools change the page, the reply just narrates.`
Expand Down
15 changes: 8 additions & 7 deletions server/ai/tools/site/writeTools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@
* site_render_snapshot, site_get_node_html).
*
* The input schemas are the single source of truth in `@core/ai`
* (`src/core/ai/toolSchemas.ts`). This module imports each `*InputSchema`
* for its tool `inputSchema`; the browser executor at
* `src/admin/pages/site/agent/executor.ts` imports the SAME schemas to
* validate each call. Neither side redeclares them, so a constraint added
* here is enforced in the browser too — at build time.
* (`src/core/ai/toolSchemas.ts`). This module imports each provider-facing
* `*InputSchema`; the browser executor imports the same schemas for validation.
* `site_apply_css` has one deliberate second layer: providers receive a flat
* object because Anthropic rejects root schema composition, then the executor
* validates the call against the exact `ApplyCssExecutionInputSchema` union.
* Both CSS layers reuse the same field schemas in the shared leaf.
*/

import {
Expand Down Expand Up @@ -191,7 +192,7 @@ const applyCssTool: AiTool = {
execution: 'browser',
requiredCapabilities: SITE_STYLE_CAPS,
description:
'Author or edit CSS — the single tool for ALL styling that isn\'t attached inline. Pass real CSS text and it is parsed and UPSERTED into the site: a bare `.foo { … }` selector creates or edits a reusable class (bound to class="foo"); ANY other selector — descendant (`.hero a`), child (`nav > li`), pseudo-class/element (`a:hover`, `.card::before`), attribute, element (`h1`) — creates or edits an ambient rule that attaches by matching, no class attribute needed. `@media` queries fold into per-breakpoint overrides (matched against the site breakpoints); other `@media`/`@supports`/`@container` round-trip as reusable conditions. Re-applying a selector MERGES onto the existing rule, so this both creates new styles and edits existing ones (e.g. `.hero a:hover { color: var(--primary) }` to restyle an existing descendant rule). Reference design tokens — `var(--primary)`, `var(--text-l)`, `var(--space-m)` — not raw hex/px. A reusable class is just a bare `.name` selector (a CSS identifier, no spaces). Success data: `{ cssRulesCreated, cssRulesUpdated }`.',
'Author, repair, or delete CSS rules. `operation:"merge"` plus real CSS creates missing selectors and patches only authored declarations/contexts; use it for normal additive edits. `operation:"replace"` makes each supplied selector\'s COMPLETE CSS payload authoritative, removing omitted base/context declarations while preserving rule identity, cascade order, and class assignments. `operation:"remove-properties"` removes named CSS properties from base and every context without disturbing other declarations. `operation:"delete"` removes whole rules by exact emitted selector and detaches deleted classes. Selector identity is exact: `.grad`, `.hero .grad`, and `.grad, .hero .grad` are different rules; copy the full selector from site_read_document before destructive operations. Bare `.foo` rules are reusable classes; descendant/pseudo/element/grouped selectors are ambient rules. `@media`/`@supports`/`@container`, vendor properties, custom properties, and `!important` round-trip. Reference design tokens rather than repeated literals. Success data uses `cssRulesCreated`, `cssRulesUpdated`, `cssRulesDeleted`, and/or `cssPropertiesRemoved`.',
inputSchema: ApplyCssInputSchema,
}

Expand Down Expand Up @@ -400,7 +401,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 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.",
"Inspect the rendered canvas. Returns viewport and node geometry, image-load status, overflow/visibility warnings, and key computed styles including color, background image/clip, and WebKit text-mask values. Those computed fields expose cascade failures such as a shorthand resetting `background-clip:text`; compare them with source CSS from site_read_document. When the provider supports image-bearing tool results, a screenshot is also attached. Pass any configured `breakpointId` to render a readiness-aware one-shot frame at that exact width, independent of collapsed/disabled frames or Live mode (defaults to active; unknown ids error). Pass `nodeId` to crop the document to that node while preserving its HTML/body/ancestor paint; omit it for the full page.",
inputSchema: RenderSnapshotInputSchema,
}

Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/agent/agentSlice.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -715,7 +715,7 @@ describe('sendAgentMessage — request lifecycle', () => {
type: 'toolRequest',
requestId: 'req-7',
toolName: 'site_apply_css',
input: { css: '.pricing-card { padding: 24px; }' },
input: { operation: 'merge', css: '.pricing-card { padding: 24px; }' },
},
{ type: 'done' },
]),
Expand Down
Loading
Loading