@ggui-ai/ui-gen 0.15.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/generation-dispatch.d.ts +21 -4
- package/dist/adapters/generation-dispatch.js +913 -350
- package/dist/adapters/generation-dispatch.js.map +1 -1
- package/dist/adapters/index.js +26 -22
- package/dist/adapters/index.js.map +1 -1
- package/dist/advanced/index.d.ts +17 -12
- package/dist/advanced/index.js +966 -374
- package/dist/advanced/index.js.map +1 -1
- package/dist/boilerplate.d.ts +16 -1
- package/dist/boilerplate.js +579 -245
- package/dist/boilerplate.js.map +1 -1
- package/dist/check/index.d.ts +17 -3
- package/dist/check/index.js +32 -25
- package/dist/check/index.js.map +1 -1
- package/dist/{create-ui-generator-D6TKuv9V.d.ts → create-ui-generator-CCIeQaEe.d.ts} +77 -10
- package/dist/design-mode-BHEE7flj.d.ts +67 -0
- package/dist/{dispatch-BuZfTh9r.d.ts → dispatch-BA5dJcg-.d.ts} +4 -1
- package/dist/evaluation/axis-checks/index.d.ts +4 -3
- package/dist/evaluation/axis-checks/index.js +3 -1
- package/dist/evaluation/axis-checks/index.js.map +1 -1
- package/dist/evaluation/axis-checks/registry.d.ts +2 -1
- package/dist/evaluation/axis-checks/registry.js +1 -0
- package/dist/evaluation/axis-checks/registry.js.map +1 -1
- package/dist/evaluation/index.d.ts +143 -3
- package/dist/evaluation/index.js +3866 -92
- package/dist/evaluation/index.js.map +1 -1
- package/dist/{generate-BTfvELgl.d.ts → generate-8L_bc2iP.d.ts} +10 -1
- package/dist/harness/check/runtime-render/index.d.ts +2 -1
- package/dist/harness/index.d.ts +2 -1
- package/dist/harness/index.js +589 -248
- package/dist/harness/index.js.map +1 -1
- package/dist/harness/llm-trace-sink.d.ts +3 -3
- package/dist/harness/llm-trace-sink.js.map +1 -1
- package/dist/harness/prompts.d.ts +2 -1
- package/dist/harness/prompts.js +3 -0
- package/dist/harness/prompts.js.map +1 -1
- package/dist/harness/result-types.d.ts +17 -5
- package/dist/harness/result-types.js.map +1 -1
- package/dist/harness/runtime.d.ts +19 -3
- package/dist/harness/runtime.js +602 -261
- package/dist/harness/runtime.js.map +1 -1
- package/dist/harness/types-public.d.ts +28 -1
- package/dist/harness/types-public.js +589 -248
- package/dist/harness/types-public.js.map +1 -1
- package/dist/harness/validator-trace-sink.d.ts +2 -1
- package/dist/index.d.ts +8 -4
- package/dist/index.js +973 -372
- package/dist/index.js.map +1 -1
- package/dist/patch.d.ts +2 -1
- package/dist/{prompts-BVhtH3M1.d.ts → prompts-k6m6reqe.d.ts} +11 -1
- package/dist/providers/index.d.ts +2 -2
- package/dist/providers/index.js.map +1 -1
- package/dist/tools/get-primitives-ts.d.ts +1 -1
- package/dist/tools/get-primitives-ts.js +1 -1
- package/dist/tools/get-primitives-ts.js.map +1 -1
- package/dist/{types-public-CkNWEov9.d.ts → types-public-D24JLJgl.d.ts} +49 -5
- package/dist/validation/index.d.ts +1 -1
- package/dist/validation/index.js +4 -4
- package/dist/validation/index.js.map +1 -1
- package/dist/workflows.d.ts +2 -1
- package/package.json +12 -12
- package/src/adapters/generation-dispatch.ts +57 -4
- package/src/adapters/provider-router.ts +7 -6
- package/src/adapters/tools.ts +5 -5
- package/src/advanced/generator.ts +11 -10
- package/src/advanced/index.ts +5 -4
- package/src/boilerplate/free-design-prompt.ts +329 -0
- package/src/boilerplate/generate.ts +10 -1
- package/src/boilerplate/hard-sections.ts +242 -0
- package/src/boilerplate/render.ts +32 -8
- package/src/boilerplate/system-prompt.ts +81 -229
- package/src/boilerplate/templates/base-free.tsx.tmpl +24 -0
- package/src/check/run-tier0.ts +80 -20
- package/src/coding-agent/tools.ts +8 -2
- package/src/contract-context.ts +28 -0
- package/src/create-harness.ts +11 -0
- package/src/create-ui-generator.ts +71 -25
- package/src/design-mode.ts +182 -0
- package/src/design-system-docs.ts +17 -13
- package/src/evaluation/axis-checks/dispatch.ts +4 -0
- package/src/evaluation/axis-checks/extras.ts +5 -0
- package/src/evaluation/cost-tracker.ts +2 -3
- package/src/evaluation/index.ts +46 -2
- package/src/evaluation/llm-evaluator.ts +77 -21
- package/src/evaluation/types-public.ts +103 -8
- package/src/evaluation/visual-evaluator.ts +301 -29
- package/src/harness/coding/dupe-break.ts +0 -0
- package/src/harness/coding/init-session.ts +1 -0
- package/src/harness/coding/run-coding-turn.ts +3 -0
- package/src/harness/coding/run-eval-round.ts +15 -2
- package/src/harness/llm-trace-sink.ts +3 -3
- package/src/harness/result-types.ts +15 -3
- package/src/harness/runtime.ts +18 -0
- package/src/harness/types-public.ts +27 -0
- package/src/index.ts +26 -0
- package/src/internal/extract-call-signatures.ts +2 -2
- package/src/providers/bedrock.ts +4 -4
- package/src/run-check.ts +1 -0
- package/src/tools/get-primitives-ts.ts +1 -1
- package/src/validation/allowed-imports.ts +8 -2
- package/src/validation/primitives.ts +1 -1
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
// packages/ui-gen/src/boilerplate/free-design-prompt.ts
|
|
2
|
+
//
|
|
3
|
+
// The `designMode: 'free'` system prompt. Same skeleton contract as the
|
|
4
|
+
// constrained builder — deterministic assembly from caller-supplied
|
|
5
|
+
// injection fields — but the DESIGN vocabulary is relaxed: no primitive
|
|
6
|
+
// prop enums, no branded-palette / 60-30-10 / aesthetic recipes, no
|
|
7
|
+
// "never raw HTML" rule, no mandatory primitive catalog. Everything that
|
|
8
|
+
// is a HARD contract is shared verbatim with the constrained prompt via
|
|
9
|
+
// `./hard-sections.ts`:
|
|
10
|
+
// Protocol Notes · Contract surface · Defensive coding · gesture
|
|
11
|
+
// routing · Anti-patterns · Cross-reference rules · gadget catalog ·
|
|
12
|
+
// contextSpec · Data Parameterization · Component Structure · the
|
|
13
|
+
// wire-hooks reference.
|
|
14
|
+
// What this file ADDS (the constraint set the free arm is given
|
|
15
|
+
// explicitly): design freedom · color stays tokenized · where this
|
|
16
|
+
// renders (host envelope) · the rendering canvas · budgets · imports +
|
|
17
|
+
// security · the responsiveness invariant · the a11y OUTCOME bar written
|
|
18
|
+
// for raw elements · the one-gesture-surface invariant restated for raw
|
|
19
|
+
// elements · a regenerated quality checklist.
|
|
20
|
+
|
|
21
|
+
import { consumedTokenManifest } from "@ggui-ai/design/themes";
|
|
22
|
+
import { describeCanvas, type CanvasClass } from "../design-mode.js";
|
|
23
|
+
import {
|
|
24
|
+
ANTI_PATTERNS,
|
|
25
|
+
COMPONENT_STRUCTURE,
|
|
26
|
+
CONTEXT_SPEC_SECTION,
|
|
27
|
+
CONTRACT_SURFACE,
|
|
28
|
+
CROSS_REFERENCE_RULES,
|
|
29
|
+
DATA_PARAMETERIZATION,
|
|
30
|
+
DEFENSIVE_CODING,
|
|
31
|
+
GESTURE_ROUTING,
|
|
32
|
+
PROTOCOL_NOTES,
|
|
33
|
+
renderGadgetCatalogSection,
|
|
34
|
+
} from "./hard-sections.js";
|
|
35
|
+
|
|
36
|
+
export interface FreeDesignPromptInputs {
|
|
37
|
+
readonly userRequest: string;
|
|
38
|
+
readonly canvas: CanvasClass;
|
|
39
|
+
/** Pre-rendered `## Shape Guidance` section (empty string when no axis fragments fired). */
|
|
40
|
+
readonly axisSection: string;
|
|
41
|
+
/** Pre-rendered P0/P1/P2 criteria summary (free-mode criteria). */
|
|
42
|
+
readonly criteriaBlock: string;
|
|
43
|
+
readonly wireDoc: string;
|
|
44
|
+
readonly primitivesDoc: string;
|
|
45
|
+
/** Rendered gadget-catalog table (from `formatGadgetsSection`). */
|
|
46
|
+
readonly gadgetsSection: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The one-gesture-surface-per-binding invariant, restated for raw
|
|
51
|
+
* elements. The constrained prompt carries the same rule in
|
|
52
|
+
* design-package vocabulary (`INTERACTIVE_TRAITS`).
|
|
53
|
+
*/
|
|
54
|
+
export const FREE_GESTURE_SURFACE = `## One gesture surface per action binding
|
|
55
|
+
|
|
56
|
+
Every \`useAction\` binding is wired to exactly ONE interactive surface. Interactive content MUST NOT contain other interactive content that dispatches the same binding — a click on the inner control bubbles to the outer handler and the action fires TWICE, so a toggle-style action silently reverts the user's change. Concretely: never put a \`<button>\`, \`<a>\`, \`<input>\`, \`<select>\`, an element with \`role="button"\`, or any element with its own \`onClick\`, inside a \`<div>\` / \`<li>\` / \`<tr>\` / \`<Card>\` whose own \`onClick\` calls the same binding. EITHER the whole row is the trigger (interactive container, no interactive children) OR an inner control is the trigger (plain container) — never both. Tier-0 fails the nested shape with \`double-wired-action:certain\`. A row with a checkbox: put the action on the checkbox's \`onChange\` and leave the row plain. \`stopPropagation\` is not a fix — pick one surface.`;
|
|
57
|
+
|
|
58
|
+
export const FREE_DESIGN_FREEDOM = `## Design freedom
|
|
59
|
+
|
|
60
|
+
You are not required to compose from the \`@ggui-ai/design\` primitives. Compose with raw HTML elements (\`<div>\`, \`<section>\`, \`<button>\`, \`<input>\`, \`<ul>\`, \`<table>\`, …), inline \`style\` props and \`<style>\` blocks — regular CSS, \`@media\` and \`@container\` queries, \`@keyframes\`, \`:hover\` / \`:focus-visible\` (the host CSP permits \`'unsafe-inline'\` styles). Any layout: CSS grid, flexbox, absolute positioning, sticky headers. The runtime injects NO Tailwind and NO reset stylesheet — a raw element renders unstyled until your CSS styles it, so bring the CSS you need. Use class selectors in \`<style>\` blocks (an \`#id\` selector that reads as hex digits trips the color check).
|
|
61
|
+
|
|
62
|
+
\`@ggui-ai/design\` stays importable — \`import { Skeleton, EmptyState, Icon } from '@ggui-ai/design'\` — use a primitive when it saves work, skip it when it constrains you. Its props are documented in the optional catalog at the end of this prompt.
|
|
63
|
+
|
|
64
|
+
Spacing, typography, radius, shadow geometry and layout values MAY be literals: \`padding: '20px'\`, \`fontSize: 'clamp(1rem, 2vw, 1.5rem)'\`, \`borderRadius: 12\`, \`gap: '0.75rem'\`, \`boxShadow: '0 8px 24px var(--ggui-color-outlineVariant)'\`. Color is the one exception — next section.`;
|
|
65
|
+
|
|
66
|
+
const COLOR_PREFIX = "--ggui-color-";
|
|
67
|
+
|
|
68
|
+
/** Ramp families the manifest MAY carry, in the order the prompt lists them. */
|
|
69
|
+
const RAMP_FAMILIES = ["primary", "neutral", "success", "warning", "error", "info"] as const;
|
|
70
|
+
|
|
71
|
+
export interface ColorRamp {
|
|
72
|
+
readonly family: string;
|
|
73
|
+
/** Numeric steps present in the manifest, ascending — the WHOLE ramp. */
|
|
74
|
+
readonly steps: readonly number[];
|
|
75
|
+
/** Whether the bare `--ggui-color-<family>` token also exists. */
|
|
76
|
+
readonly bare: boolean;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Semantic (non-ramp) color roles, grouped for the prose. A group renders
|
|
81
|
+
* only the names the manifest actually carries and is dropped when none
|
|
82
|
+
* do; manifest roles not named here land under "other roles".
|
|
83
|
+
*/
|
|
84
|
+
const SEMANTIC_ROLES: ReadonlyArray<{ readonly names: readonly string[]; readonly role: string }> = [
|
|
85
|
+
{ names: ["ground", "onGround"], role: "the page / chat canvas behind everything + its text" },
|
|
86
|
+
{ names: ["container", "onContainer"], role: "a card, a bubble, a panel + body text on it" },
|
|
87
|
+
{ names: ["elevated", "onElevated"], role: "popover, menu, modal, toast — floats above a container" },
|
|
88
|
+
{ names: ["sunken", "onSunken"], role: "wells, inputs at rest, code blocks + secondary text" },
|
|
89
|
+
{ names: ["ground-subtle", "ground-gradient"], role: "quiet canvas fills" },
|
|
90
|
+
{ names: ["onPrimary"], role: "text on the brand color" },
|
|
91
|
+
{ names: ["outline", "outlineVariant"], role: "borders + dividers" },
|
|
92
|
+
{ names: ["onError"], role: "text on an error fill" },
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
/** Split the manifest's `--ggui-color-*` names into ramps (family + numeric steps) and semantic roles. */
|
|
96
|
+
export function colorVocabularyFromManifest(manifest: readonly string[]): {
|
|
97
|
+
readonly ramps: readonly ColorRamp[];
|
|
98
|
+
readonly roles: readonly string[];
|
|
99
|
+
} {
|
|
100
|
+
const colors = manifest.filter((t) => t.startsWith(COLOR_PREFIX)).map((t) => t.slice(COLOR_PREFIX.length));
|
|
101
|
+
const ramps: ColorRamp[] = [];
|
|
102
|
+
const claimed = new Set<string>();
|
|
103
|
+
for (const family of RAMP_FAMILIES) {
|
|
104
|
+
const steps: number[] = [];
|
|
105
|
+
for (const name of colors) {
|
|
106
|
+
const m = new RegExp(`^${family}-(\\d+)$`).exec(name);
|
|
107
|
+
if (m) {
|
|
108
|
+
steps.push(Number(m[1]));
|
|
109
|
+
claimed.add(name);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (steps.length === 0) continue;
|
|
113
|
+
steps.sort((a, b) => a - b);
|
|
114
|
+
const bare = colors.includes(family);
|
|
115
|
+
if (bare) claimed.add(family);
|
|
116
|
+
ramps.push({ family, steps, bare });
|
|
117
|
+
}
|
|
118
|
+
const roles = colors.filter((n) => !claimed.has(n)).sort();
|
|
119
|
+
return { ramps, roles };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function renderRamp(r: ColorRamp): string {
|
|
123
|
+
return `\`${r.family}-${r.steps.join("/")}\`${r.bare ? ` (plus bare \`${r.family}\`)` : ""}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The one hard styling rule of the free arm, with the color vocabulary
|
|
128
|
+
* rendered FROM the closed consumed-token manifest (the same set
|
|
129
|
+
* `tokens:off-manifest-token` checks against) — every ramp step and
|
|
130
|
+
* every role the prose names exists on `:root`; nothing is implied by a
|
|
131
|
+
* `50…800` range that the manifest does not carry step for step.
|
|
132
|
+
*/
|
|
133
|
+
export function renderFreeColorRule(manifest: readonly string[] = consumedTokenManifest): string {
|
|
134
|
+
const { ramps, roles } = colorVocabularyFromManifest(manifest);
|
|
135
|
+
const brand = ramps.filter((r) => r.family === "primary" || r.family === "neutral");
|
|
136
|
+
const state = ramps.filter((r) => r.family !== "primary" && r.family !== "neutral");
|
|
137
|
+
const roleLines: string[] = [];
|
|
138
|
+
const named = new Set<string>();
|
|
139
|
+
for (const g of SEMANTIC_ROLES) {
|
|
140
|
+
const present = g.names.filter((n) => roles.includes(n));
|
|
141
|
+
if (present.length === 0) continue;
|
|
142
|
+
for (const n of present) named.add(n);
|
|
143
|
+
roleLines.push(`${present.map((n) => `\`${n}\``).join(" / ")} (${g.role})`);
|
|
144
|
+
}
|
|
145
|
+
const other = roles.filter((n) => !named.has(n));
|
|
146
|
+
if (other.length > 0) roleLines.push(`other roles: ${other.map((n) => `\`${n}\``).join(", ")}`);
|
|
147
|
+
|
|
148
|
+
const vocabulary = [
|
|
149
|
+
roleLines.length > 0 ? `- Semantic roles cover most needs: ${roleLines.join("; ")}.` : "",
|
|
150
|
+
brand.length > 0 ? `- Brand + neutral ramps (every step that exists, nothing else): ${brand.map(renderRamp).join(", ")}.` : "",
|
|
151
|
+
state.length > 0 ? `- State ramps (every step that exists, nothing else): ${state.map(renderRamp).join(", ")}.` : "",
|
|
152
|
+
"- A step not listed does not exist and renders unset; pick the nearest listed step.",
|
|
153
|
+
]
|
|
154
|
+
.filter((l) => l.length > 0)
|
|
155
|
+
.join("\n");
|
|
156
|
+
|
|
157
|
+
return `## Color stays tokenized (the one hard styling rule)
|
|
158
|
+
|
|
159
|
+
Brand-bearing color — text, backgrounds, borders, fills, gradient stops, focus rings, shadow tints — comes ONLY from the closed \`--ggui-color-*\` token manifest, referenced BARE: \`color: 'var(--ggui-color-onContainer)'\`, \`background: 'var(--ggui-color-primary-50)'\`, \`border: '1px solid var(--ggui-color-outline)'\`, \`linear-gradient(135deg, var(--ggui-color-primary-500), var(--ggui-color-primary-700))\`.
|
|
160
|
+
|
|
161
|
+
- No hex (\`#7c3aed\`), no \`rgb()\` / \`rgba()\` / \`hsl()\` / \`hsla()\`, no CSS named colors (\`red\`, \`slategray\`) — anywhere in the file, including \`<style>\` blocks and shadows. Tier-0 fails them (\`tokens:hex-color\`, \`tokens:hardcoded-color-fn\`, \`tokens:named-color\`). \`transparent\`, \`currentColor\` and \`inherit\` are fine.
|
|
162
|
+
- No literal fallback inside ANY token reference: \`var(--ggui-color-primary-600)\` bare, never \`var(--ggui-color-primary-600, #0284c7)\` — the runtime injects every token on \`:root\`, and a fallback paints the wrong brand exactly when the operator's theme matters (\`tokens:token-fallback\`). This holds for every \`--ggui-*\` token you reference, not only colors.
|
|
163
|
+
- Only names from the manifest (listed at the end of this prompt) exist — an invented \`--ggui-color-*\` name renders unset (\`tokens:off-manifest-token\`).
|
|
164
|
+
- Shadows: \`var(--ggui-shape-shadow-sm|md|lg|xl)\`, or a literal offset/blur whose color is a token — never an \`rgba()\` tint.
|
|
165
|
+
- Why: the operator switches the whole app's theme (light, dark, branded) by redefining these variables. A component that reads them is on-brand everywhere; one that hardcodes color is off-brand everywhere but the default.
|
|
166
|
+
|
|
167
|
+
The color vocabulary (rendered from the manifest — these names and steps, exactly):
|
|
168
|
+
${vocabulary}`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export const FREE_COLOR_RULE = renderFreeColorRule();
|
|
172
|
+
|
|
173
|
+
export const FREE_HOST_ENVELOPE = `## Where this renders
|
|
174
|
+
|
|
175
|
+
The compiled component is loaded into a sandboxed iframe by an MCP Apps host (chat clients, the ggui console, embedded panels):
|
|
176
|
+
|
|
177
|
+
- **Sandbox**: \`allow-scripts allow-forms\`, no same-origin. The only channel to the outside is \`postMessage\` through the wire hooks — the component never talks to a server itself.
|
|
178
|
+
- **No network from the component**: \`fetch\`, \`XMLHttpRequest\`, \`WebSocket\`, dynamic \`import()\` fail tier-0 and the CSP (\`connect-src 'none'\` on spec hosts). No \`<link rel="stylesheet">\`, no \`@import url()\`, no CDN scripts, no web fonts from a CDN — use \`var(--ggui-font-family-sans)\` / \`var(--ggui-font-family-mono)\` (system stacks) or a generic family.
|
|
179
|
+
- **No host document access**: \`window.*\`, \`document.*\` (other than the intercepted \`requestFullscreen\` / \`exitFullscreen\`), \`localStorage\`, \`sessionStorage\`, \`navigator\`, \`location\`, \`history\`, \`innerHTML\` / \`dangerouslySetInnerHTML\`, \`<script>\` — all rejected.
|
|
180
|
+
- **Props arrive through \`_meta["ai.ggui/render"]\`** on the host side and reach you as \`props\`; later repaints (\`ggui_amend\`) update the SAME mounted component — see "Defensive coding".
|
|
181
|
+
- **Display modes**: the host advertises \`availableDisplayModes\` (\`inline\` | \`fullscreen\` | \`pip\`); \`el.requestFullscreen()\` asks the host to switch — never assume it will.
|
|
182
|
+
- **Size**: the host sets the iframe's \`containerDimensions\` once per render; there is no live resize notification — a resize is a re-render, so layout must be fluid (\`width: 100%\`, \`max-width\`, no fixed width) and hold at any width inside the canvas range above.`;
|
|
183
|
+
|
|
184
|
+
export const FREE_BUDGETS = `## Budgets (deterministic caps)
|
|
185
|
+
|
|
186
|
+
- Source ≤ 50 KB and ≤ 500 lines.
|
|
187
|
+
- Compiled bundle ≤ 2 MB.
|
|
188
|
+
- One file, one \`export default function Component(props: Props)\`.`;
|
|
189
|
+
|
|
190
|
+
export const FREE_IMPORTS_AND_SECURITY = `## Imports & security (what you may import, what you may not call)
|
|
191
|
+
|
|
192
|
+
Import ONLY from: \`react\`, \`react-dom\`, \`@ggui-ai/design\` (single barrel — no \`/primitives\` / \`/components\` subpaths), \`@ggui-ai/wire\`, \`@ggui-ai/gadgets\`, or a gadget package the contract declares — never \`@ggui-ai/wire/internal\`. The iframe runtime rewrites exactly these specifiers to in-bundle shims; ANY other import fails at module evaluation and blanks the iframe (tier-0 \`imports\` fail). No \`require()\`.
|
|
193
|
+
|
|
194
|
+
Never call \`eval()\`, \`Function()\`, \`fetch()\` or the host-document APIs listed under "Where this renders" — tier-0 \`security\` fails them and the sandbox blocks them anyway.`;
|
|
195
|
+
|
|
196
|
+
export const FREE_RESPONSIVE_INVARIANT = `## Responsive invariant
|
|
197
|
+
|
|
198
|
+
The component must fit the host-supplied iframe width at every point of the canvas range: \`width: 100%\` (plus \`max-width\` where you want to cap), never a fixed width, no horizontal scroll. Use \`@media\` / \`@container\` queries, \`clamp()\`, \`minmax()\`, \`auto-fit\` grids and \`flex-wrap\` freely. Compact chrome on \`xs-chat-card\` (the host draws the card border and shadow); own the chrome on the fullscreen canvases (\`mobile-fullscreen-small\`, \`md\`, \`lg\`, \`xl\`).`;
|
|
199
|
+
|
|
200
|
+
export const FREE_ACCESSIBILITY = `## Accessibility (REQUIRED)
|
|
201
|
+
|
|
202
|
+
With raw elements YOU own the semantics — nothing is accessible by construction unless you import a \`@ggui-ai/design\` primitive (those carry their own roles / labels / keyboard handling; don't re-declare ARIA on them).
|
|
203
|
+
|
|
204
|
+
1. **Interactive = a real control.** A click target is a \`<button type="button">\` or an \`<a href>\` — never a bare \`<div onClick>\`. If a non-button element must be clickable, add \`role="button"\`, \`tabIndex={0}\` and an \`onKeyDown\` handling Enter/Space.
|
|
205
|
+
2. **Form inputs have labels.** \`<label htmlFor={id}>\` paired with the input's \`id\`, or \`aria-label\`; errors via \`aria-invalid\` + \`aria-describedby\`.
|
|
206
|
+
3. **Images have \`alt\`** (\`alt=""\` for decorative). Icon-only buttons need \`aria-label\`; decorative icons and emoji are \`aria-hidden="true"\`.
|
|
207
|
+
4. **Live & streaming data** (a \`useStream\` \`.latest\` value, counters, clocks, flashing prices) sits in an \`aria-live="polite"\` (or \`role="status"\`) region so screen readers announce the change.
|
|
208
|
+
5. **Headings nest**: one \`<h1>\` per screen, \`<h2>\` for sections, \`<h3>\` below — never skipped or inverted.
|
|
209
|
+
6. **Busy state is announced**: \`<button disabled={isLoading} aria-busy={isLoading}>{isLoading ? 'Submitting…' : 'Submit'}</button>\`.
|
|
210
|
+
7. **Stateful controls announce state**: \`role="checkbox"\` + \`aria-checked\` on a toggleable row, \`aria-pressed\` on a toggle button, \`aria-selected\` on the chosen item, \`aria-expanded\` on an open/close affordance. Styling (strikethrough, color, a check mark) is never the only carrier of state.
|
|
211
|
+
8. **Focus is visible** — style \`:focus-visible\` with a token color; never \`outline: none\` without a replacement.`;
|
|
212
|
+
|
|
213
|
+
export const FREE_QUALITY_CHECKLIST = `## Quality Checklist (verify before returning)
|
|
214
|
+
|
|
215
|
+
- [ ] Imports ONLY from: react, react-dom, @ggui-ai/design, @ggui-ai/wire, @ggui-ai/gadgets (+ contract gadget packages)
|
|
216
|
+
- [ ] ZERO literal colors — every color is a bare \`var(--ggui-color-*)\` from the manifest; no hex / rgb() / hsl() / named colors, no fallbacks — in \`style\` props AND \`<style>\` blocks
|
|
217
|
+
- [ ] Fluid width (\`width: 100%\`, \`max-width\`), holds across the whole canvas range, no horizontal scroll
|
|
218
|
+
- [ ] Typed Props interface exported; request-specific data is a prop with default
|
|
219
|
+
- [ ] Every \`useAction\` binding is wired to exactly ONE gesture surface — no handler on both a parent and a child
|
|
220
|
+
- [ ] Real controls (\`<button>\`, \`<a>\`, \`<input>\` + \`<label>\`), \`alt\` on images, \`aria-label\` on icon-only buttons
|
|
221
|
+
- [ ] State is ARIA-expressed (\`aria-checked\` / \`aria-pressed\` / \`aria-selected\` / \`aria-expanded\`) — never style-only
|
|
222
|
+
- [ ] Live/streaming regions wrapped in \`aria-live="polite"\`
|
|
223
|
+
- [ ] Headings nest — one \`<h1>\`, then \`<h2>\` / \`<h3>\` — never skipped or inverted
|
|
224
|
+
- [ ] No \`fetch\` / \`eval\` / \`window\` / \`document\` / \`localStorage\` / external stylesheets or fonts
|
|
225
|
+
- [ ] Source ≤ 50 KB / ≤ 500 lines; wire hooks (\`useAction\`, \`useStream\`, \`useGguiContext\`) kept and consumed`;
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Note appended to the axis `## Shape Guidance` section in free mode:
|
|
229
|
+
* the fragments name design-package components as defaults; here they
|
|
230
|
+
* are optional while the behavioural rules and crash anti-patterns
|
|
231
|
+
* stay mandatory.
|
|
232
|
+
*/
|
|
233
|
+
export const FREE_SHAPE_GUIDANCE_NOTE =
|
|
234
|
+
"In this mode the design-package components named above (`<Stepper>`, `<Modal>`, `<Card>`, `<Row>`, `<CardGrid>`) are defaults, not requirements — any composition that satisfies the behavioural rule (track the step index in state, a close affordance + focus trap, `key={item.id}`, no horizontal scroll) is acceptable. The crash anti-patterns are mandatory.";
|
|
235
|
+
|
|
236
|
+
interface TokenGroup {
|
|
237
|
+
readonly label: string;
|
|
238
|
+
readonly prefixes: readonly string[];
|
|
239
|
+
readonly note: string;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const TOKEN_GROUPS: readonly TokenGroup[] = [
|
|
243
|
+
{ label: "Color (mandatory for every color)", prefixes: ["--ggui-color-"], note: "" },
|
|
244
|
+
{ label: "Spacing (optional — literals are fine)", prefixes: ["--ggui-spacing-"], note: "" },
|
|
245
|
+
{ label: "Typography (optional)", prefixes: ["--ggui-font-"], note: "" },
|
|
246
|
+
{ label: "Radius (optional)", prefixes: ["--ggui-shape-radius-"], note: "" },
|
|
247
|
+
{ label: "Shadow (optional)", prefixes: ["--ggui-shape-shadow-"], note: "" },
|
|
248
|
+
];
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* The theme token vocabulary as REFERENCE DATA — derived from the closed
|
|
252
|
+
* consumed-token manifest (the same set `tokens:off-manifest-token`
|
|
253
|
+
* checks against), so the prompt can never teach a name the runtime
|
|
254
|
+
* does not define. Grouped by prefix; anything outside the known
|
|
255
|
+
* prefixes lands under "Other".
|
|
256
|
+
*/
|
|
257
|
+
export function renderTokenVocabulary(manifest: readonly string[] = consumedTokenManifest): string {
|
|
258
|
+
const sorted = [...manifest].sort();
|
|
259
|
+
const claimed = new Set<string>();
|
|
260
|
+
const lines: string[] = [
|
|
261
|
+
"### Reference (optional): theme token vocabulary",
|
|
262
|
+
"",
|
|
263
|
+
"The closed manifest — the ONLY `--ggui-*` names the runtime defines on `:root`. Color names are mandatory for color; the rest are optional conveniences.",
|
|
264
|
+
"",
|
|
265
|
+
];
|
|
266
|
+
for (const group of TOKEN_GROUPS) {
|
|
267
|
+
const names = sorted.filter((t) => group.prefixes.some((p) => t.startsWith(p)));
|
|
268
|
+
if (names.length === 0) continue;
|
|
269
|
+
for (const n of names) claimed.add(n);
|
|
270
|
+
lines.push(`- **${group.label}**: ${names.map((n) => `\`${n}\``).join(", ")}`);
|
|
271
|
+
}
|
|
272
|
+
const other = sorted.filter((t) => !claimed.has(t));
|
|
273
|
+
if (other.length > 0) {
|
|
274
|
+
lines.push(`- **Other**: ${other.map((n) => `\`${n}\``).join(", ")}`);
|
|
275
|
+
}
|
|
276
|
+
return lines.join("\n");
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** Assemble the `designMode: 'free'` system prompt. */
|
|
280
|
+
export function buildFreeDesignPrompt(inputs: FreeDesignPromptInputs): string {
|
|
281
|
+
const axisSection =
|
|
282
|
+
inputs.axisSection.length > 0
|
|
283
|
+
? `${inputs.axisSection}\n${FREE_SHAPE_GUIDANCE_NOTE}\n`
|
|
284
|
+
: "";
|
|
285
|
+
|
|
286
|
+
const head = `You are ggui's UI builder. You receive a typed boilerplate and fill it in using apply_changes.
|
|
287
|
+
|
|
288
|
+
## Your Task
|
|
289
|
+
${inputs.userRequest}
|
|
290
|
+
|
|
291
|
+
## Rendering canvas
|
|
292
|
+
${describeCanvas(inputs.canvas)}
|
|
293
|
+
|
|
294
|
+
## How It Works
|
|
295
|
+
1. Read the boilerplate — typed Props and wire hooks are pre-configured; the JSX body and its styling are yours to compose
|
|
296
|
+
2. Respond with one apply_changes call — add state, helpers, styles and JSX
|
|
297
|
+
3. If compilation or evaluation fails, you'll get errors to fix in the next turn
|
|
298
|
+
|
|
299
|
+
${inputs.criteriaBlock}
|
|
300
|
+
${axisSection}
|
|
301
|
+
`;
|
|
302
|
+
|
|
303
|
+
const sections = [
|
|
304
|
+
PROTOCOL_NOTES,
|
|
305
|
+
CONTRACT_SURFACE,
|
|
306
|
+
DEFENSIVE_CODING,
|
|
307
|
+
GESTURE_ROUTING,
|
|
308
|
+
FREE_GESTURE_SURFACE,
|
|
309
|
+
ANTI_PATTERNS,
|
|
310
|
+
CROSS_REFERENCE_RULES,
|
|
311
|
+
renderGadgetCatalogSection(inputs.gadgetsSection),
|
|
312
|
+
CONTEXT_SPEC_SECTION,
|
|
313
|
+
`## Reference: Wire Hooks\n${inputs.wireDoc}`,
|
|
314
|
+
FREE_DESIGN_FREEDOM,
|
|
315
|
+
FREE_COLOR_RULE,
|
|
316
|
+
FREE_HOST_ENVELOPE,
|
|
317
|
+
FREE_BUDGETS,
|
|
318
|
+
FREE_IMPORTS_AND_SECURITY,
|
|
319
|
+
DATA_PARAMETERIZATION,
|
|
320
|
+
COMPONENT_STRUCTURE,
|
|
321
|
+
FREE_RESPONSIVE_INVARIANT,
|
|
322
|
+
FREE_ACCESSIBILITY,
|
|
323
|
+
FREE_QUALITY_CHECKLIST,
|
|
324
|
+
renderTokenVocabulary(),
|
|
325
|
+
`### Reference (optional): \`@ggui-ai/design\` component catalog\n\nOptional — import a primitive only when it helps. Raw HTML + your own CSS is a first-class choice.\n\n${inputs.primitivesDoc}`,
|
|
326
|
+
];
|
|
327
|
+
|
|
328
|
+
return head + sections.join("\n\n") + "\n";
|
|
329
|
+
}
|
|
@@ -36,6 +36,7 @@ import type {
|
|
|
36
36
|
import { HOOK_NAME_RE, listContractGadgets } from "@ggui-ai/protocol";
|
|
37
37
|
import { renderBoilerplate } from "./render.js";
|
|
38
38
|
import { jsonSchemaTypeToTs } from "./json-schema-ts.js";
|
|
39
|
+
import { DEFAULT_DESIGN_MODE, type DesignMode } from "../design-mode.js";
|
|
39
40
|
|
|
40
41
|
/** Shell layout modes supported by the boilerplate templates. */
|
|
41
42
|
export type ShellType = "chat" | "fullscreen" | "spatial";
|
|
@@ -184,6 +185,14 @@ export function generateBoilerplate(
|
|
|
184
185
|
* point rather than an empty `useFoo()` it might delete.
|
|
185
186
|
*/
|
|
186
187
|
appGadgets?: readonly GadgetDescriptor[],
|
|
188
|
+
/**
|
|
189
|
+
* Which base template to render. `constrained` (default) pre-imports
|
|
190
|
+
* the design surface + a per-shell layout scaffold — byte-identical to
|
|
191
|
+
* the pre-`designMode` boilerplate. `free` drops both; the Props /
|
|
192
|
+
* Action / Stream types, wire hooks, contextSpec hooks and gadget
|
|
193
|
+
* imports are emitted EXACTLY as in constrained mode.
|
|
194
|
+
*/
|
|
195
|
+
designMode: DesignMode = DEFAULT_DESIGN_MODE,
|
|
187
196
|
): string {
|
|
188
197
|
// ── Props interface from data contract (data only — NO action callbacks) ──
|
|
189
198
|
// Contract shape (PropsSpec is ALWAYS the wrapper — never flat):
|
|
@@ -557,5 +566,5 @@ export function generateBoilerplate(
|
|
|
557
566
|
CONTEXT_HOOKS: contextHooks,
|
|
558
567
|
WIRE_HOOKS: hookBody,
|
|
559
568
|
AXIS_SECTIONS: composedSections ?? "",
|
|
560
|
-
});
|
|
569
|
+
}, designMode);
|
|
561
570
|
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
// packages/ui-gen/src/boilerplate/hard-sections.ts
|
|
2
|
+
//
|
|
3
|
+
// The HARD-CONTRACT sections of the coding-agent system prompt — the
|
|
4
|
+
// text that is identical in every `designMode`. Both prompt builders
|
|
5
|
+
// (`buildSystemPrompt` for `constrained`, `buildFreeDesignPrompt` for
|
|
6
|
+
// `free`) assemble from these constants, so the wire / contract / host /
|
|
7
|
+
// gadget / blueprint-reuse teaching cannot drift between the two arms.
|
|
8
|
+
//
|
|
9
|
+
// These strings were extracted VERBATIM from the pre-`designMode`
|
|
10
|
+
// `buildSystemPrompt` template; `design-mode.pin.test.ts` pins the
|
|
11
|
+
// constrained assembly byte-for-byte against the pre-extraction digest.
|
|
12
|
+
// Edit them here and both arms move together.
|
|
13
|
+
|
|
14
|
+
/** Wire-hook preservation rules (`wire_preservation` / `wire_undeclared`). */
|
|
15
|
+
export const PROTOCOL_NOTES = `## Protocol Notes
|
|
16
|
+
The boilerplate pre-declares every wire hook the contract requires (\`useAction\`, \`useStream\`, \`useGguiContext\`, plus capability hooks from \`@ggui-ai/gadgets\` when the contract declares \`clientCapabilities\`). Three rules:
|
|
17
|
+
1. **Do NOT delete any pre-declared hook.** \`self_check\` fails with \`wire_preservation:<kind>:<name>\` if you remove one.
|
|
18
|
+
2. **Consume every hook binding** somewhere in the component — in JSX, a callback, or an effect. Unused bindings fail lint with \`no-unused-vars\`.
|
|
19
|
+
3. **Do NOT invent new wire calls.** Every \`useAction('X')\`, \`useStream('X')\`, \`useGguiContext('X')\` etc. MUST correspond to a declared entry on the contract. Calling one that isn't declared fails \`self_check\` with \`wire_undeclared:<kind>:<name>\` because the runtime has no Context/registration for it and would throw at first paint. If you need a new wire surface, that's a contract authoring step the agent owns — your job is to honor what's declared.
|
|
20
|
+
|
|
21
|
+
Renaming a binding is fine — the wiring is the string-literal argument, not the identifier.`;
|
|
22
|
+
|
|
23
|
+
/** The four specs + two catalogs, the actions-vs-context placement rule and the data-vs-behavior split. */
|
|
24
|
+
export const CONTRACT_SURFACE = `## Contract surface — four specs + two catalogs
|
|
25
|
+
|
|
26
|
+
A \`DataContract\` declares everything a GguiSession exchanges with the outside world. **Four typed specs** for the four data-flow directions, **two reference catalogs** for tool / hook lookups:
|
|
27
|
+
|
|
28
|
+
| Surface | Direction | Role |
|
|
29
|
+
| -------------------- | -------------------------- | -------------------------------------------------------------------- |
|
|
30
|
+
| \`propsSpec\` | server → UI (one-shot) | Initial render values delivered once at \`ggui_render\` |
|
|
31
|
+
| \`streamSpec\` | agent → UI (many) | Typed channels for live updates via \`ggui_emit\` |
|
|
32
|
+
| \`actionSpec\` | UI → agent (events) | Discrete events driving the agent's next turn (consumed via \`ggui_consume\`) |
|
|
33
|
+
| \`contextSpec\` | UI → server (state mirror) | UI state the agent observes between turns |
|
|
34
|
+
| \`agentCapabilities.tools\` | catalog | Tools the contract references via \`actionSpec[*].nextStep\` and \`streamSpec[*].source.tool\` |
|
|
35
|
+
| \`clientCapabilities.gadgets\` | catalog | Browser-capability gadget hooks the component code mounts (e.g., \`useGeolocation\`) |
|
|
36
|
+
|
|
37
|
+
**Placement rule for inbound specs**: actions drive turns; context observes state. There is no third category.
|
|
38
|
+
|
|
39
|
+
**Data vs behavior**: the contract describes data flow; the component code describes behavior. Scroll, focus, toast, animation, clipboard write — all component code, never contract fields.`;
|
|
40
|
+
|
|
41
|
+
/** Guarding absent / late-arriving props, streams and context slots. */
|
|
42
|
+
export const DEFENSIVE_CODING = `## Defensive coding for absent / late-arriving data
|
|
43
|
+
|
|
44
|
+
Props arrive at mount and may be partial on first render; later prop changes reach the SAME mounted component via \`ggui_amend\` (in-place repaint) on the live channel. Stream channels start empty and fill over time. Context slots start at their declared default (often \`null\`). **Never assume a field exists before you read it.**
|
|
45
|
+
|
|
46
|
+
- **Optional contract props**: a \`Props\` field marked \`?\` in the boilerplate may be omitted by the agent ENTIRELY — the wire gate only enforces \`required: true\` entries, and the harness verifies your component with those props stripped. Guard them (\`?? default\`, \`?.\`, or an empty-state branch) from the FIRST render.
|
|
47
|
+
- **Array iteration**: always default to \`[]\` before \`.map\`/\`.filter\`/\`.length\`. Use \`(props.items ?? []).map(...)\` not \`props.items.map(...)\`. Same for stream.all, stream.latest, etc.
|
|
48
|
+
- **Object access**: optional-chain through nested fields. \`props.user?.name ?? 'Anonymous'\` not \`props.user.name\`.
|
|
49
|
+
- **Number ops**: default before arithmetic. \`(props.count ?? 0) + 1\` not \`props.count + 1\`.
|
|
50
|
+
- **Stream latest**: \`useStream\` returns \`{latest: T | null, all: T[], isComplete: boolean}\`. The default \`all\` is \`[]\` so it's safe to map; \`latest\` is null until the first frame arrives — guard before reading \`.foo\`.
|
|
51
|
+
- **Stream reconciliation**: when a stream event carries an \`action\` discriminant (e.g. \`create | move | edit | delete\`), the channel is a CRUD feed — your handler MUST branch on EVERY value: append on \`create\`, drop on \`delete\`, replace-by-id on \`move\` / \`edit\`. Merging only the "edit" case silently loses created and deleted items. Reconcile into the SAME state that seeds from \`props\` (e.g. \`useState(() => props.tasks ?? [])\`) so the seed data and the live feed render as one list — and handle an event whose id is not yet present (a \`create\` for an unknown item) by inserting it, not ignoring it.
|
|
52
|
+
- **Props re-sync on repaint**: props the agent owns are the source of truth for display — prefer rendering DIRECTLY from \`props\` (controlled, no local copy). When local optimistic state genuinely needs a prop seed (a click bumps a count before the agent confirms), it MUST re-sync on prop change — \`useEffect(() => setCount(props.count ?? 0), [props.count])\` — or a \`ggui_amend\` repaint applies the new props while the display keeps the stale local value. BAD: \`const [count, setCount] = useState(props.count ?? 0)\` alone. GOOD: the same seed + the one-line re-sync effect.
|
|
53
|
+
- **Loading state**: while data is still absent, render \`<Skeleton>\` placeholders — never a blank screen. \`<Skeleton variant="text" />\` for a text line, \`variant="circle"\` for an avatar slot, default \`rect\` for a block.
|
|
54
|
+
- **Empty state**: when a list or results array is empty, render \`<EmptyState title="…" description="…" />\` — a region that renders nothing when empty looks broken to the user.
|
|
55
|
+
|
|
56
|
+
Unhandled \`Cannot read properties of undefined\` errors trip the iframe error boundary and the user sees "Something went wrong" — a regression class the runtime can't recover from.`;
|
|
57
|
+
|
|
58
|
+
/** Which wire surface each user gesture maps to (actions, context, gadgets, links, fullscreen). */
|
|
59
|
+
export const GESTURE_ROUTING = `## Picking the right primitive for user gestures
|
|
60
|
+
|
|
61
|
+
Choose by what the user is DOING, not where the result goes — the runtime handles the routing.
|
|
62
|
+
|
|
63
|
+
| Gesture intent | LLM writes | Notes |
|
|
64
|
+
| -------------- | ---------- | ----- |
|
|
65
|
+
| Fire a server-side action | \`useAction(name)\` + call \`dispatch(name, payload)\` | Every action is agent-routed. The runtime emits an event on \`ggui_consume\`; the agent reacts on its next turn. If the contract entry declares \`nextStep: 'X'\`, that names the tool the agent SHOULD call next — advisory hint forwarded as event metadata. |
|
|
66
|
+
| Surface state to the agent's context | the auto-generated \`setSlotName\` setter (from the boilerplate's \`useGguiContext\` line) | The runtime owns useState + Provider; the boilerplate emits one \`const [slot, setSlot] = useGguiContext<T>('slot')\` line per declared \`contextSpec\` slot. Write plain JSX, no \`useState\`, no Provider wrap. Every value change auto-flows to the host LLM (debounced). One-way client → agent — see "Observable state via \`contextSpec\`" below. |
|
|
67
|
+
| Use a browser capability (camera, mic, geolocation, clipboard, file picker, notifications) | call the hook the contract declared, e.g., \`const loc = useGeolocation();\` and \`await loc.start()\` | The contract's \`clientCapabilities.gadgets\` declares which gadget exports the UI uses. The hook implementations live in \`@ggui-ai/gadgets\` (or a third-party package named in the \`Package\` column). Read \`status\` ("idle" / "prompting" / "active" / "completed" / "denied" / "error") to gate UI, and thread the resolved \`value\` into a contextSpec slot or actionSpec payload if the agent needs to see it. |
|
|
68
|
+
| Open external link | Plain \`<a href="https://...">\` (or \`target="_blank"\`) | External cross-origin clicks are intercepted and routed through the host (security warnings, app-internal navigation, audit). Same-origin links and \`#fragment\` jumps stay native. |
|
|
69
|
+
| Toggle fullscreen / chrome | Plain \`el.requestFullscreen()\` / \`document.exitFullscreen()\` | The native browser API is intercepted; the host adjusts iframe chrome accordingly. Returns a resolved promise so \`.then()\` / \`await\` chains don't break. |
|
|
70
|
+
|
|
71
|
+
Every gesture fires a uniform server-side audit envelope (\`ggui_runtime_submit_action\`) so operators see all three patterns in RenderInspector with the same shape.
|
|
72
|
+
|
|
73
|
+
**Don't import wire hooks for link / display-mode.** \`useAction\` is the only wire hook for user gestures; links and fullscreen use plain HTML / browser APIs.
|
|
74
|
+
|
|
75
|
+
**All actions are agent-routed.** Every action emits an event the agent reacts to on its next turn via \`ggui_consume\`. The optional \`nextStep: '<tool>'\` field on an \`actionSpec\` entry is a HINT naming the tool the agent SHOULD call next — the contract author's recommendation, NOT a binding directive. The agent decides whether to honor it. If you want to declare a tool catalog entry the contract references, add it to \`agentCapabilities.tools[<name>]\` with input/output schemas; the cross-ref linter rejects dangling \`nextStep\` values that don't resolve to a declared catalog entry.`;
|
|
76
|
+
|
|
77
|
+
/** `as={Trait}` mechanics + the never-nest-two-interactive-elements rule in design-package vocabulary (constrained mode only — `free` mode restates the invariant for raw elements). */
|
|
78
|
+
export const INTERACTIVE_TRAITS = `## Making a primitive interactive — \`as={Trait}\`
|
|
79
|
+
|
|
80
|
+
Structural primitives (\`Box\`, \`Stack\`, \`Row\`, \`Card\`) have NO \`onClick\` by default. Add interactivity with the \`as\` prop — a trait, not a wrapper:
|
|
81
|
+
|
|
82
|
+
\`\`\`tsx
|
|
83
|
+
<Card as={Clickable} onClick={() => dispatch('select', { id })}
|
|
84
|
+
hoverStyle={{ boxShadow: 'var(--ggui-shape-shadow-lg)' }}>…</Card>
|
|
85
|
+
\`\`\`
|
|
86
|
+
|
|
87
|
+
- \`as={Clickable}\` → \`onClick\` + keyboard activation (Enter/Space) + \`role="button"\` + \`hoverStyle\`/\`activeStyle\`/\`cursor\`.
|
|
88
|
+
- \`as={Hoverable}\` → \`hoverStyle\` only (no click). \`as={Pressable}\` → \`onPress\` + \`pressStyle\`.
|
|
89
|
+
|
|
90
|
+
\`as={Trait}\` is a PROP — it does NOT re-nest the JSX. Never write \`<Clickable>…</Clickable>\` around a primitive; put \`as={Clickable}\` on the primitive itself. The trait carries the keyboard + ARIA wiring, so don't hand-write \`onKeyDown\` / \`role\`. Trait components (\`Clickable\`, \`Hoverable\`, \`Pressable\`) import from \`@ggui-ai/design\` like everything else — the boilerplate already imports them.
|
|
91
|
+
|
|
92
|
+
**Semantic components are already interactive** — \`Button\` (\`onClick\`), \`Link\` (\`href\`), \`Input\` / \`Select\` (\`onChange\`). Use their own props; never put \`as\` on them. \`Text\` picks its element with \`is\` (\`<Text is="label">\`), not \`as\`.
|
|
93
|
+
|
|
94
|
+
**Never nest two interactive elements.** Interactive content MUST NOT contain other interactive content — a gesture on the inner control bubbles to the outer one and fires BOTH handlers (one user click → the action dispatched twice). Do NOT put a \`Button\`, \`Checkbox\`, \`Input\`, \`Select\`, \`Link\`, or another \`as={Clickable}\` primitive inside a \`Card\` / \`Box\` / \`Row\` / \`Stack\` that is itself \`as={Clickable}\`. Wire each \`useAction\` callback to exactly ONE surface: EITHER the whole card is the trigger (interactive container, no interactive children) OR an inner control is the trigger (plain container, no \`as={Clickable}\`) — never both. A row with a checkbox: put the action on the \`Checkbox onChange\` and leave the row plain.
|
|
95
|
+
|
|
96
|
+
**\`Text\` / \`Heading\` accept NO event handlers and NO \`as\` — only \`style\` / \`className\` plus their own typed props.** \`onClick\`, \`onDoubleClick\`, \`as={Clickable}\`, \`color\` are all type errors on \`Text\`. When the request says a label is "clickable", "editable", "edit on click / double-click", or "tap to …", do ONE of these — never put the handler on \`Text\`:
|
|
97
|
+
|
|
98
|
+
\`\`\`tsx
|
|
99
|
+
// Click-to-edit a label: wrap the Text in a Clickable structural primitive.
|
|
100
|
+
<Box as={Clickable} onClick={() => setEditingId(task.id)}
|
|
101
|
+
style={{ cursor: 'pointer' }}>
|
|
102
|
+
<Text weight="semibold">{task.title}</Text>
|
|
103
|
+
</Box>
|
|
104
|
+
|
|
105
|
+
// Or pair the label with an explicit edit Button (clearer affordance).
|
|
106
|
+
<Row gap="xs" align="center">
|
|
107
|
+
<Text weight="semibold">{task.title}</Text>
|
|
108
|
+
<Button variant="ghost" size="xs" aria-label="Edit title"
|
|
109
|
+
onClick={() => setEditingId(task.id)}>Edit</Button>
|
|
110
|
+
</Row>
|
|
111
|
+
|
|
112
|
+
// In edit mode, swap the Text for an Input.
|
|
113
|
+
{editingId === task.id
|
|
114
|
+
? <Input value={draftTitle} onChange={setDraftTitle} label="Task title" />
|
|
115
|
+
: <Text weight="semibold">{task.title}</Text>}
|
|
116
|
+
\`\`\``;
|
|
117
|
+
|
|
118
|
+
/** Retired contract identifiers the linter rejects. */
|
|
119
|
+
export const ANTI_PATTERNS = `## Anti-patterns — DO NOT WRITE
|
|
120
|
+
|
|
121
|
+
The following identifiers / shapes are RETIRED from the contract surface as of 2026-05-11. Pre-2026-05-11 examples in your training data may include them; do not reproduce. The linter / CI grep gate rejects:
|
|
122
|
+
|
|
123
|
+
- \`useWiredTool\`, \`useClientTool\` — retired hooks. Replace with \`useAction\` (events) and the named hook from \`@ggui-ai/gadgets\` (browser capabilities).
|
|
124
|
+
- \`dispatch: { kind: 'tool', tool: '...' }\` / \`dispatch: { kind: 'agent', intendedTool: '...' }\` — retired discriminated-union. Use the flat optional \`nextStep?: '<tool>'\` instead.
|
|
125
|
+
- \`mode: 'host-routed'\` / \`mode: 'tool'\` — retired \`mode\` field. Same fix: flat \`nextStep?\`.
|
|
126
|
+
- \`broadcast: {...}\` on the contract — retired top-level field. Use \`streamSpec[channel].source: {tool, args?}\` to declare a tool-fed channel.
|
|
127
|
+
- \`wiredTools\` / \`agentTools\` (top-level) — retired catalog names. Use \`agentCapabilities.tools\`.
|
|
128
|
+
- \`clientTools\` / \`clientCapabilities.capabilities\` — retired catalog shapes. Use \`clientCapabilities.gadgets\` (entries declare hooks, not RPC).
|
|
129
|
+
- \`@ggui-ai/client-tools\` — retired package name. Import gadget hooks from \`@ggui-ai/gadgets\`.
|
|
130
|
+
- \`intendedTool\` — retired. Use \`nextStep\` (flat).
|
|
131
|
+
- \`props: { properties: {...} }\` as a CONTRACT field — retired. The contract field is \`propsSpec\` (the wire \`props\` field on render / update still carries VALUES).`;
|
|
132
|
+
|
|
133
|
+
/** `nextStep` / `source.tool` catalog cross-refs. */
|
|
134
|
+
export const CROSS_REFERENCE_RULES = `## Cross-reference rules
|
|
135
|
+
|
|
136
|
+
When you declare a reference, also declare the catalog entry it points at:
|
|
137
|
+
|
|
138
|
+
- \`actionSpec[X].nextStep = 'fetch_inbox'\` → \`agentCapabilities.tools.fetch_inbox = { toolInfo: { inputSchema, description?, outputSchema? }, usage?, example? }\` MUST exist. Cross-ref code: \`CTR_REF_NEXT_STEP\`.
|
|
139
|
+
- \`streamSpec[X].source.tool = 'list_messages'\` → \`agentCapabilities.tools.list_messages\` MUST exist. Cross-ref code: \`CTR_REF_STREAM_SOURCE\`.
|
|
140
|
+
- The catalog entry's schemas MUST be a superset of the referencing spec's schema. Cross-ref code: \`CTR_SCHEMA_INCOMPAT\`.`;
|
|
141
|
+
|
|
142
|
+
/** `clientCapabilities` — the registered gadget catalog table (rendered per app). */
|
|
143
|
+
export function renderGadgetCatalogSection(gadgetsSection: string): string {
|
|
144
|
+
return `## clientCapabilities — registered catalog
|
|
145
|
+
|
|
146
|
+
${gadgetsSection}
|
|
147
|
+
|
|
148
|
+
Each hook conforms to \`GadgetHook<TOutput, TOptions>\`: call \`start(opts?)\` to fire, read \`{value, status, error, stop?}\`. \`status\` walks through \`idle → prompting → active|completed\` or routes to \`denied\` / \`error\` on failure.
|
|
149
|
+
|
|
150
|
+
3rd-party plugins (Leaflet maps, Mapbox, Stripe, Chart.js, …) are registered via \`createGguiGadget\` from \`@ggui-ai/gadgets\` and surface in this same table when the operator has added them to \`App.gadgets\`. Reference any registered hook by name — render validation rejects hooks not in this catalog with \`gadget_not_registered\`.`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** `contextSpec` — auto-generated `useGguiContext` slots, one-way client → agent. */
|
|
154
|
+
export const CONTEXT_SPEC_SECTION = `## Observable state via \`contextSpec\`
|
|
155
|
+
|
|
156
|
+
When the contract declares \`contextSpec\`, the boilerplate auto-generates one \`useGguiContext\` call per slot at the top of your component. The runtime owns the underlying \`useState\` and the Provider tree — **you do NOT write \`useState\` or any \`<Provider>\` wrap yourself**:
|
|
157
|
+
|
|
158
|
+
\`\`\`tsx
|
|
159
|
+
import { useGguiContext } from '@ggui-ai/wire';
|
|
160
|
+
|
|
161
|
+
export default function Component(props: Props) {
|
|
162
|
+
// AUTO-GENERATED — do not remove or rename:
|
|
163
|
+
const [currentStep, setCurrentStep] = useGguiContext<number>('currentStep');
|
|
164
|
+
const [draftText, setDraftText] = useGguiContext<string>('draftText');
|
|
165
|
+
|
|
166
|
+
// Plain JSX. No Provider wrap. The runtime already wrapped your
|
|
167
|
+
// component in nested SingleSlotProviders before this code ran.
|
|
168
|
+
return (
|
|
169
|
+
<Container>
|
|
170
|
+
<Text>Step {currentStep}</Text>
|
|
171
|
+
<Input value={draftText} onChange={(e) => setDraftText(e.target.value)} />
|
|
172
|
+
<Button onClick={() => setCurrentStep((s) => s + 1)}>Next</Button>
|
|
173
|
+
</Container>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
\`\`\`
|
|
177
|
+
|
|
178
|
+
For every declared slot you have **\`slotName\` + \`setSlotName\`** in scope:
|
|
179
|
+
- **Read** the value to render: \`<Text>Step {currentStep}</Text>\`
|
|
180
|
+
- **Write** via the setter: \`setCurrentStep(s => s + 1)\` (in callbacks, effects, anywhere)
|
|
181
|
+
|
|
182
|
+
Every value change is mirrored to the host LLM's context automatically (debounced, default 300ms — adjustable per-slot via \`entry.debounceMs\` in the contract). The agent sees the user's interaction state — drafts, current step, hover, selection — without you calling any API.
|
|
183
|
+
|
|
184
|
+
**When to use the auto-generated state.** Any slot the contract declared. If \`contextSpec.draftText\` exists, bind \`<Input value={draftText} onChange={e => setDraftText(e.target.value)}>\` so the agent sees the typing live. If \`contextSpec.currentStep\` exists, render the step indicator from \`currentStep\` and bump it via \`setCurrentStep\` in your "next" callback.
|
|
185
|
+
|
|
186
|
+
**When NOT to use it.** Local UI state the contract did NOT declare — \`isDropdownOpen\`, hover flags, animation phase, ephemeral toggles. For those, use a plain \`useState\` directly. The runtime ignores undeclared state.
|
|
187
|
+
|
|
188
|
+
**\`contextSpec\` direction is one-way: client → agent.** The agent uses \`propsSpec\` (via \`ggui_amend\` for in-place repaints of this mount, or \`ggui_update\` when it mints a new history card) and \`streamSpec\` (via the live channel) to push state TO the client. Don't try to write to the agent via \`contextSpec\` — there is no return path.
|
|
189
|
+
|
|
190
|
+
**Schema mismatches drop silently.** If you set a value that doesn't match the slot's schema (e.g. a string into a \`{type: 'number'}\` slot), the runtime logs a dev \`console.warn\` and skips the post. Make sure your setter calls produce values that match the declared shape.`;
|
|
191
|
+
|
|
192
|
+
/** Blueprint-reuse contract — request data as defaulted props, exported typed `Props`. Identical in both modes. */
|
|
193
|
+
export const DATA_PARAMETERIZATION = `## Data Parameterization (CRITICAL)
|
|
194
|
+
|
|
195
|
+
Generated components are CACHED blueprints reused across requests. NEVER hardcode request-specific data (names, cities, numbers, dates) into the component body. Define data as default prop values so the blueprint works for ANY similar request:
|
|
196
|
+
|
|
197
|
+
\`\`\`tsx
|
|
198
|
+
// BAD — hardcoded, only works for Tokyo
|
|
199
|
+
const city = "Tokyo";
|
|
200
|
+
const temp = 18;
|
|
201
|
+
|
|
202
|
+
// GOOD — parameterized via props with defaults from the request
|
|
203
|
+
interface Props {
|
|
204
|
+
city?: string;
|
|
205
|
+
temperature?: number;
|
|
206
|
+
}
|
|
207
|
+
export default function WeatherCard({ city = "Tokyo", temperature = 18 }: Props) {
|
|
208
|
+
// A controller can override for Seoul, Paris, etc.
|
|
209
|
+
}
|
|
210
|
+
\`\`\`
|
|
211
|
+
|
|
212
|
+
Rules:
|
|
213
|
+
1. All request-specific data → props with defaults. City names, tickers, user names, dates, counts.
|
|
214
|
+
2. Layout and styling are universal. Colors, spacing, structure — these are the reusable part.
|
|
215
|
+
3. Default values come from the current request — so the component renders correctly standalone.
|
|
216
|
+
4. Props interface must be typed and exported.`;
|
|
217
|
+
|
|
218
|
+
/** Helper-component structure guidance (JSX depth 3–5, helpers above `Component`). Identical in both modes. */
|
|
219
|
+
export const COMPONENT_STRUCTURE = `## Component Structure
|
|
220
|
+
|
|
221
|
+
Keep JSX nesting depth to 3–5 levels. When deeper, extract repeated/complex sections into helper components — named functions defined above the main Component in the same file. Helpers take data + callbacks via props; they don't own state.
|
|
222
|
+
|
|
223
|
+
\`\`\`tsx
|
|
224
|
+
import { useState } from 'react';
|
|
225
|
+
import { Container, Card, Stack, Text, Button, Input } from '@ggui-ai/design';
|
|
226
|
+
|
|
227
|
+
interface Props {
|
|
228
|
+
onSubmit?: (data: unknown) => void;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function ItemCard({ item, onEdit }: { item: Item; onEdit: (id: string) => void }) {
|
|
232
|
+
return <Card padding="md">…</Card>;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export default function GeneratedComponent({ onSubmit }: Props) {
|
|
236
|
+
return (
|
|
237
|
+
<Container>
|
|
238
|
+
{items.map((item) => <ItemCard key={item.id} item={item} onEdit={handleEdit} />)}
|
|
239
|
+
</Container>
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
\`\`\``;
|