@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.
Files changed (101) hide show
  1. package/dist/adapters/generation-dispatch.d.ts +21 -4
  2. package/dist/adapters/generation-dispatch.js +913 -350
  3. package/dist/adapters/generation-dispatch.js.map +1 -1
  4. package/dist/adapters/index.js +26 -22
  5. package/dist/adapters/index.js.map +1 -1
  6. package/dist/advanced/index.d.ts +17 -12
  7. package/dist/advanced/index.js +966 -374
  8. package/dist/advanced/index.js.map +1 -1
  9. package/dist/boilerplate.d.ts +16 -1
  10. package/dist/boilerplate.js +579 -245
  11. package/dist/boilerplate.js.map +1 -1
  12. package/dist/check/index.d.ts +17 -3
  13. package/dist/check/index.js +32 -25
  14. package/dist/check/index.js.map +1 -1
  15. package/dist/{create-ui-generator-D6TKuv9V.d.ts → create-ui-generator-CCIeQaEe.d.ts} +77 -10
  16. package/dist/design-mode-BHEE7flj.d.ts +67 -0
  17. package/dist/{dispatch-BuZfTh9r.d.ts → dispatch-BA5dJcg-.d.ts} +4 -1
  18. package/dist/evaluation/axis-checks/index.d.ts +4 -3
  19. package/dist/evaluation/axis-checks/index.js +3 -1
  20. package/dist/evaluation/axis-checks/index.js.map +1 -1
  21. package/dist/evaluation/axis-checks/registry.d.ts +2 -1
  22. package/dist/evaluation/axis-checks/registry.js +1 -0
  23. package/dist/evaluation/axis-checks/registry.js.map +1 -1
  24. package/dist/evaluation/index.d.ts +143 -3
  25. package/dist/evaluation/index.js +3866 -92
  26. package/dist/evaluation/index.js.map +1 -1
  27. package/dist/{generate-BTfvELgl.d.ts → generate-8L_bc2iP.d.ts} +10 -1
  28. package/dist/harness/check/runtime-render/index.d.ts +2 -1
  29. package/dist/harness/index.d.ts +2 -1
  30. package/dist/harness/index.js +589 -248
  31. package/dist/harness/index.js.map +1 -1
  32. package/dist/harness/llm-trace-sink.d.ts +3 -3
  33. package/dist/harness/llm-trace-sink.js.map +1 -1
  34. package/dist/harness/prompts.d.ts +2 -1
  35. package/dist/harness/prompts.js +3 -0
  36. package/dist/harness/prompts.js.map +1 -1
  37. package/dist/harness/result-types.d.ts +17 -5
  38. package/dist/harness/result-types.js.map +1 -1
  39. package/dist/harness/runtime.d.ts +19 -3
  40. package/dist/harness/runtime.js +602 -261
  41. package/dist/harness/runtime.js.map +1 -1
  42. package/dist/harness/types-public.d.ts +28 -1
  43. package/dist/harness/types-public.js +589 -248
  44. package/dist/harness/types-public.js.map +1 -1
  45. package/dist/harness/validator-trace-sink.d.ts +2 -1
  46. package/dist/index.d.ts +8 -4
  47. package/dist/index.js +973 -372
  48. package/dist/index.js.map +1 -1
  49. package/dist/patch.d.ts +2 -1
  50. package/dist/{prompts-BVhtH3M1.d.ts → prompts-k6m6reqe.d.ts} +11 -1
  51. package/dist/providers/index.d.ts +2 -2
  52. package/dist/providers/index.js.map +1 -1
  53. package/dist/tools/get-primitives-ts.d.ts +1 -1
  54. package/dist/tools/get-primitives-ts.js +1 -1
  55. package/dist/tools/get-primitives-ts.js.map +1 -1
  56. package/dist/{types-public-CkNWEov9.d.ts → types-public-D24JLJgl.d.ts} +49 -5
  57. package/dist/validation/index.d.ts +1 -1
  58. package/dist/validation/index.js +4 -4
  59. package/dist/validation/index.js.map +1 -1
  60. package/dist/workflows.d.ts +2 -1
  61. package/package.json +12 -12
  62. package/src/adapters/generation-dispatch.ts +57 -4
  63. package/src/adapters/provider-router.ts +7 -6
  64. package/src/adapters/tools.ts +5 -5
  65. package/src/advanced/generator.ts +11 -10
  66. package/src/advanced/index.ts +5 -4
  67. package/src/boilerplate/free-design-prompt.ts +329 -0
  68. package/src/boilerplate/generate.ts +10 -1
  69. package/src/boilerplate/hard-sections.ts +242 -0
  70. package/src/boilerplate/render.ts +32 -8
  71. package/src/boilerplate/system-prompt.ts +81 -229
  72. package/src/boilerplate/templates/base-free.tsx.tmpl +24 -0
  73. package/src/check/run-tier0.ts +80 -20
  74. package/src/coding-agent/tools.ts +8 -2
  75. package/src/contract-context.ts +28 -0
  76. package/src/create-harness.ts +11 -0
  77. package/src/create-ui-generator.ts +71 -25
  78. package/src/design-mode.ts +182 -0
  79. package/src/design-system-docs.ts +17 -13
  80. package/src/evaluation/axis-checks/dispatch.ts +4 -0
  81. package/src/evaluation/axis-checks/extras.ts +5 -0
  82. package/src/evaluation/cost-tracker.ts +2 -3
  83. package/src/evaluation/index.ts +46 -2
  84. package/src/evaluation/llm-evaluator.ts +77 -21
  85. package/src/evaluation/types-public.ts +103 -8
  86. package/src/evaluation/visual-evaluator.ts +301 -29
  87. package/src/harness/coding/dupe-break.ts +0 -0
  88. package/src/harness/coding/init-session.ts +1 -0
  89. package/src/harness/coding/run-coding-turn.ts +3 -0
  90. package/src/harness/coding/run-eval-round.ts +15 -2
  91. package/src/harness/llm-trace-sink.ts +3 -3
  92. package/src/harness/result-types.ts +15 -3
  93. package/src/harness/runtime.ts +18 -0
  94. package/src/harness/types-public.ts +27 -0
  95. package/src/index.ts +26 -0
  96. package/src/internal/extract-call-signatures.ts +2 -2
  97. package/src/providers/bedrock.ts +4 -4
  98. package/src/run-check.ts +1 -0
  99. package/src/tools/get-primitives-ts.ts +1 -1
  100. package/src/validation/allowed-imports.ts +8 -2
  101. package/src/validation/primitives.ts +1 -1
@@ -9,6 +9,7 @@
9
9
  import { readFileSync } from 'node:fs';
10
10
  import { resolve, dirname } from 'node:path';
11
11
  import { fileURLToPath } from 'node:url';
12
+ import { DEFAULT_DESIGN_MODE, type DesignMode } from '../design-mode.js';
12
13
 
13
14
  const __dirname = dirname(fileURLToPath(import.meta.url));
14
15
 
@@ -35,20 +36,41 @@ const TEMPLATE_DIRS = [
35
36
  resolve(__dirname, '..', '..', '..', '..', 'src', 'boilerplate', 'templates'),
36
37
  ];
37
38
 
38
- let baseCache: string | null = null;
39
+ /**
40
+ * One base template per design mode. `constrained` pre-imports the whole
41
+ * design surface and scaffolds a per-shell layout; `free` keeps the
42
+ * wire / Props / gadget anchors EXACTLY and drops the design mass-import
43
+ * and the layout scaffold (the design package stays importable).
44
+ */
45
+ const BASE_TEMPLATE_FILE: Readonly<Record<DesignMode, string>> = {
46
+ constrained: 'base.tsx.tmpl',
47
+ free: 'base-free.tsx.tmpl',
48
+ };
49
+
50
+ /** Free-mode body — no design-package scaffold, just a fluid root. */
51
+ const FREE_LAYOUT = [
52
+ " <div style={{ width: '100%' }}>",
53
+ ' {/* implement your UI here */}',
54
+ ' </div>',
55
+ ].join('\n');
56
+
57
+ const baseCache = new Map<DesignMode, string>();
39
58
  const layoutCache = new Map<string, string>();
40
59
 
41
- function loadBase(): string {
42
- if (baseCache) return baseCache;
60
+ function loadBase(designMode: DesignMode): string {
61
+ const cached = baseCache.get(designMode);
62
+ if (cached !== undefined) return cached;
43
63
 
64
+ const file = BASE_TEMPLATE_FILE[designMode];
44
65
  for (const dir of TEMPLATE_DIRS) {
45
66
  try {
46
- baseCache = readFileSync(resolve(dir, 'base.tsx.tmpl'), 'utf-8');
47
- return baseCache;
67
+ const content = readFileSync(resolve(dir, file), 'utf-8');
68
+ baseCache.set(designMode, content);
69
+ return content;
48
70
  } catch { continue; }
49
71
  }
50
72
 
51
- throw new Error('No base.tsx.tmpl found');
73
+ throw new Error(`No ${file} found`);
52
74
  }
53
75
 
54
76
  function loadLayout(shellType: string, screen: string): string {
@@ -96,9 +118,11 @@ export function renderBoilerplate(
96
118
  shellType: string,
97
119
  screen: string,
98
120
  markers: BoilerplateMarkers,
121
+ designMode: DesignMode = DEFAULT_DESIGN_MODE,
99
122
  ): string {
100
- let template = loadBase();
101
- const layout = loadLayout(shellType, screen);
123
+ let template = loadBase(designMode);
124
+ const layout =
125
+ designMode === 'free' ? FREE_LAYOUT : loadLayout(shellType, screen);
102
126
  template = template.replace('{{LAYOUT}}', layout);
103
127
 
104
128
  for (const [key, value] of Object.entries(markers)) {
@@ -22,6 +22,26 @@ import type {
22
22
  GadgetComponentExport,
23
23
  } from "@ggui-ai/protocol";
24
24
  import { buildCodingCriteriaSummary } from "../evaluation/types-public.js";
25
+ import {
26
+ DEFAULT_DESIGN_MODE,
27
+ canvasForRendering,
28
+ type DesignMode,
29
+ type CanvasClass,
30
+ } from "../design-mode.js";
31
+ import { buildFreeDesignPrompt } from "./free-design-prompt.js";
32
+ import {
33
+ ANTI_PATTERNS,
34
+ COMPONENT_STRUCTURE,
35
+ CONTEXT_SPEC_SECTION,
36
+ CONTRACT_SURFACE,
37
+ CROSS_REFERENCE_RULES,
38
+ DATA_PARAMETERIZATION,
39
+ DEFENSIVE_CODING,
40
+ GESTURE_ROUTING,
41
+ INTERACTIVE_TRAITS,
42
+ PROTOCOL_NOTES,
43
+ renderGadgetCatalogSection,
44
+ } from "./hard-sections.js";
25
45
  import {
26
46
  extractCallSignaturesFromDts,
27
47
  extractComponentPropsFromDts,
@@ -92,6 +112,20 @@ export interface SystemPromptInputs {
92
112
  * callers (the section stays byte-identical).
93
113
  */
94
114
  gadgetTypes?: Readonly<Record<string, string>>;
115
+ /**
116
+ * Which triad the prompt teaches. `constrained` (default) is today's
117
+ * design-system prompt, byte-identical to the pre-`designMode`
118
+ * output; `free` relaxes only the design vocabulary — see
119
+ * `./free-design-prompt.ts` and `../design-mode.ts`.
120
+ */
121
+ designMode?: DesignMode;
122
+ /**
123
+ * Rendering canvas class — `free` mode only. Rendered as an explicit
124
+ * width-range sentence so the agent is TOLD its canvas. When omitted
125
+ * it is derived from `shellType` × `screen`. Ignored in `constrained`
126
+ * mode (that prompt keeps its shell/screen descriptors verbatim).
127
+ */
128
+ canvas?: CanvasClass;
95
129
  }
96
130
 
97
131
  const SHELL_DESCRIPTIONS: Record<string, string> = {
@@ -283,7 +317,9 @@ export function buildSystemPrompt(inputs: SystemPromptInputs): string {
283
317
  const shellDesc = SHELL_DESCRIPTIONS[shell] ?? SHELL_DESCRIPTIONS.fullscreen;
284
318
  const screenDesc = SCREEN_DESCRIPTIONS[scr] ?? SCREEN_DESCRIPTIONS.universal;
285
319
 
286
- const criteriaBlock = inputs.criteriaBlock ?? buildCodingCriteriaSummary();
320
+ const designMode = inputs.designMode ?? DEFAULT_DESIGN_MODE;
321
+ const criteriaBlock =
322
+ inputs.criteriaBlock ?? buildCodingCriteriaSummary(designMode);
287
323
  const pitfallsBlock = inputs.pitfallsBlock ?? "";
288
324
  const designSystemDocs = inputs.designSystemDocs ?? "";
289
325
  const primitivesDoc = inputs.primitivesDoc ?? "";
@@ -304,6 +340,18 @@ export function buildSystemPrompt(inputs: SystemPromptInputs): string {
304
340
  ? `\n## Shape Guidance\n${inputs.axisDelta}\n`
305
341
  : "";
306
342
 
343
+ if (designMode === "free") {
344
+ return buildFreeDesignPrompt({
345
+ userRequest: inputs.userRequest,
346
+ canvas: inputs.canvas ?? canvasForRendering(inputs.shellType, inputs.screen),
347
+ axisSection,
348
+ criteriaBlock,
349
+ wireDoc,
350
+ primitivesDoc,
351
+ gadgetsSection,
352
+ });
353
+ }
354
+
307
355
  return `You are ggui's UI builder. You receive a typed boilerplate and fill it in using apply_changes.
308
356
 
309
357
  ## Your Task
@@ -320,172 +368,19 @@ ${inputs.userRequest}
320
368
 
321
369
  ${criteriaBlock}
322
370
  ${axisSection}
323
- ## Protocol Notes
324
- 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:
325
- 1. **Do NOT delete any pre-declared hook.** \`self_check\` fails with \`wire_preservation:<kind>:<name>\` if you remove one.
326
- 2. **Consume every hook binding** somewhere in the component — in JSX, a callback, or an effect. Unused bindings fail lint with \`no-unused-vars\`.
327
- 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.
328
-
329
- Renaming a binding is fine — the wiring is the string-literal argument, not the identifier.
330
-
331
- ## Contract surface — four specs + two catalogs
332
-
333
- 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:
334
-
335
- | Surface | Direction | Role |
336
- | -------------------- | -------------------------- | -------------------------------------------------------------------- |
337
- | \`propsSpec\` | server → UI (one-shot) | Initial render values delivered once at \`ggui_render\` |
338
- | \`streamSpec\` | agent → UI (many) | Typed channels for live updates via \`ggui_emit\` |
339
- | \`actionSpec\` | UI → agent (events) | Discrete events driving the agent's next turn (consumed via \`ggui_consume\`) |
340
- | \`contextSpec\` | UI → server (state mirror) | UI state the agent observes between turns |
341
- | \`agentCapabilities.tools\` | catalog | Tools the contract references via \`actionSpec[*].nextStep\` and \`streamSpec[*].source.tool\` |
342
- | \`clientCapabilities.gadgets\` | catalog | Browser-capability gadget hooks the component code mounts (e.g., \`useGeolocation\`) |
343
-
344
- **Placement rule for inbound specs**: actions drive turns; context observes state. There is no third category.
345
-
346
- **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.
347
-
348
- ## Defensive coding for absent / late-arriving data
349
-
350
- 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.**
351
-
352
- - **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.
353
- - **Array iteration**: always default to \`[]\` before \`.map\`/\`.filter\`/\`.length\`. Use \`(props.items ?? []).map(...)\` not \`props.items.map(...)\`. Same for stream.all, stream.latest, etc.
354
- - **Object access**: optional-chain through nested fields. \`props.user?.name ?? 'Anonymous'\` not \`props.user.name\`.
355
- - **Number ops**: default before arithmetic. \`(props.count ?? 0) + 1\` not \`props.count + 1\`.
356
- - **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\`.
357
- - **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.
358
- - **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.
359
- - **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.
360
- - **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.
361
-
362
- 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.
363
-
364
- ## Picking the right primitive for user gestures
365
-
366
- Choose by what the user is DOING, not where the result goes — the runtime handles the routing.
367
-
368
- | Gesture intent | LLM writes | Notes |
369
- | -------------- | ---------- | ----- |
370
- | 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. |
371
- | 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. |
372
- | 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. |
373
- | 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. |
374
- | 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. |
375
-
376
- 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.
377
-
378
- **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.
379
-
380
- **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.
381
-
382
- ## Making a primitive interactive — \`as={Trait}\`
383
-
384
- Structural primitives (\`Box\`, \`Stack\`, \`Row\`, \`Card\`) have NO \`onClick\` by default. Add interactivity with the \`as\` prop — a trait, not a wrapper:
385
-
386
- \`\`\`tsx
387
- <Card as={Clickable} onClick={() => dispatch('select', { id })}
388
- hoverStyle={{ boxShadow: 'var(--ggui-shape-shadow-lg)' }}>…</Card>
389
- \`\`\`
390
-
391
- - \`as={Clickable}\` → \`onClick\` + keyboard activation (Enter/Space) + \`role="button"\` + \`hoverStyle\`/\`activeStyle\`/\`cursor\`.
392
- - \`as={Hoverable}\` → \`hoverStyle\` only (no click). \`as={Pressable}\` → \`onPress\` + \`pressStyle\`.
393
-
394
- \`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.
395
-
396
- **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\`.
397
-
398
- **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.
399
-
400
- **\`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\`:
401
-
402
- \`\`\`tsx
403
- // Click-to-edit a label: wrap the Text in a Clickable structural primitive.
404
- <Box as={Clickable} onClick={() => setEditingId(task.id)}
405
- style={{ cursor: 'pointer' }}>
406
- <Text weight="semibold">{task.title}</Text>
407
- </Box>
408
-
409
- // Or pair the label with an explicit edit Button (clearer affordance).
410
- <Row gap="xs" align="center">
411
- <Text weight="semibold">{task.title}</Text>
412
- <Button variant="ghost" size="xs" aria-label="Edit title"
413
- onClick={() => setEditingId(task.id)}>Edit</Button>
414
- </Row>
415
-
416
- // In edit mode, swap the Text for an Input.
417
- {editingId === task.id
418
- ? <Input value={draftTitle} onChange={setDraftTitle} label="Task title" />
419
- : <Text weight="semibold">{task.title}</Text>}
420
- \`\`\`
421
-
422
- ## Anti-patterns — DO NOT WRITE
423
-
424
- 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:
425
-
426
- - \`useWiredTool\`, \`useClientTool\` — retired hooks. Replace with \`useAction\` (events) and the named hook from \`@ggui-ai/gadgets\` (browser capabilities).
427
- - \`dispatch: { kind: 'tool', tool: '...' }\` / \`dispatch: { kind: 'agent', intendedTool: '...' }\` — retired discriminated-union. Use the flat optional \`nextStep?: '<tool>'\` instead.
428
- - \`mode: 'host-routed'\` / \`mode: 'tool'\` — retired \`mode\` field. Same fix: flat \`nextStep?\`.
429
- - \`broadcast: {...}\` on the contract — retired top-level field. Use \`streamSpec[channel].source: {tool, args?}\` to declare a tool-fed channel.
430
- - \`wiredTools\` / \`agentTools\` (top-level) — retired catalog names. Use \`agentCapabilities.tools\`.
431
- - \`clientTools\` / \`clientCapabilities.capabilities\` — retired catalog shapes. Use \`clientCapabilities.gadgets\` (entries declare hooks, not RPC).
432
- - \`@ggui-ai/client-tools\` — retired package name. Import gadget hooks from \`@ggui-ai/gadgets\`.
433
- - \`intendedTool\` — retired. Use \`nextStep\` (flat).
434
- - \`props: { properties: {...} }\` as a CONTRACT field — retired. The contract field is \`propsSpec\` (the wire \`props\` field on render / update still carries VALUES).
435
-
436
- ## Cross-reference rules
437
-
438
- When you declare a reference, also declare the catalog entry it points at:
439
-
440
- - \`actionSpec[X].nextStep = 'fetch_inbox'\` → \`agentCapabilities.tools.fetch_inbox = { toolInfo: { inputSchema, description?, outputSchema? }, usage?, example? }\` MUST exist. Cross-ref code: \`CTR_REF_NEXT_STEP\`.
441
- - \`streamSpec[X].source.tool = 'list_messages'\` → \`agentCapabilities.tools.list_messages\` MUST exist. Cross-ref code: \`CTR_REF_STREAM_SOURCE\`.
442
- - The catalog entry's schemas MUST be a superset of the referencing spec's schema. Cross-ref code: \`CTR_SCHEMA_INCOMPAT\`.
443
-
444
- ## clientCapabilities — registered catalog
445
-
446
- ${gadgetsSection}
447
-
448
- 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.
449
-
450
- 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\`.
451
-
452
- ## Observable state via \`contextSpec\`
453
-
454
- 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**:
455
-
456
- \`\`\`tsx
457
- import { useGguiContext } from '@ggui-ai/wire';
458
-
459
- export default function Component(props: Props) {
460
- // AUTO-GENERATED — do not remove or rename:
461
- const [currentStep, setCurrentStep] = useGguiContext<number>('currentStep');
462
- const [draftText, setDraftText] = useGguiContext<string>('draftText');
463
-
464
- // Plain JSX. No Provider wrap. The runtime already wrapped your
465
- // component in nested SingleSlotProviders before this code ran.
466
- return (
467
- <Container>
468
- <Text>Step {currentStep}</Text>
469
- <Input value={draftText} onChange={(e) => setDraftText(e.target.value)} />
470
- <Button onClick={() => setCurrentStep((s) => s + 1)}>Next</Button>
471
- </Container>
472
- );
473
- }
474
- \`\`\`
475
-
476
- For every declared slot you have **\`slotName\` + \`setSlotName\`** in scope:
477
- - **Read** the value to render: \`<Text>Step {currentStep}</Text>\`
478
- - **Write** via the setter: \`setCurrentStep(s => s + 1)\` (in callbacks, effects, anywhere)
479
-
480
- 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.
481
-
482
- **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.
483
-
484
- **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.
485
-
486
- **\`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.
487
-
488
- **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.
371
+ ` +
372
+ [
373
+ PROTOCOL_NOTES,
374
+ CONTRACT_SURFACE,
375
+ DEFENSIVE_CODING,
376
+ GESTURE_ROUTING,
377
+ INTERACTIVE_TRAITS,
378
+ ANTI_PATTERNS,
379
+ CROSS_REFERENCE_RULES,
380
+ renderGadgetCatalogSection(gadgetsSection),
381
+ CONTEXT_SPEC_SECTION,
382
+ ].join("\n\n") +
383
+ `
489
384
 
490
385
  ${pitfallsBlock}
491
386
 
@@ -510,7 +405,7 @@ ${primitivesDoc}
510
405
  * primitive *names*. Exported so `prompt-type-drift.test.ts` can
511
406
  * verify its enum claims against the auto-generated primitive catalog.
512
407
  */
513
- export const DESIGN_SYSTEM_GUIDANCE = `## Imports & Component Surface
408
+ const DESIGN_SYSTEM_GUIDANCE_SURFACE_AND_TOKENS = `## Imports & Component Surface
514
409
 
515
410
  Import ONLY from: \`react\`, \`@ggui-ai/design\`, \`@ggui-ai/wire\`. The ENTIRE design system — every primitive, component, composition and trait — is exported from the single \`@ggui-ai/design\` entry: \`import { Card, Grid, Stack, Modal, Clickable } from '@ggui-ai/design'\`. There are NO subpaths (\`/primitives\`, \`/components\`, …) — never import from them. Use the design components — DO NOT use raw HTML elements (\`<button>\`, \`<input>\`, \`<div>\` for layout) or Tailwind classes; those render unstyled in the iframe runtime.
516
411
 
@@ -545,7 +440,7 @@ EXACT primitive prop values (other values are silently ignored — the design sy
545
440
  - \`<Grid columns={N} gap="md">\` — 2-D layout (rows AND columns). Reach for it for card galleries, stat grids and dashboards — NEVER hand-roll \`style={{ display: 'grid' }}\`. When the request names exact per-breakpoint counts ("3 per row on desktop, 1 on mobile"), pass a map: \`<Grid columns={{ base: 1, md: 3 }}>\` (breakpoints \`sm\`/\`md\`/\`lg\`/\`xl\`; the design system emits the media queries). For an open-ended gallery where any column count is fine, use \`<Grid minColumnWidth={220}>\` — it fits as many equal columns as the width allows. \`radius\` (Card / Box / Image) takes the scale \`none | sm | md | lg | xl\`.
546
441
  - \`<Stat label="…" value="…" delta="+12%" trend="up">\` — KPI display (label + big value + trend-coloured delta + optional \`icon\`). \`trend\` is \`up | down | neutral\` (delta renders green / red / muted). Reach for it for any "show a number" UI; drop several into a \`<Grid>\` for a stat grid instead of hand-building label+value pairs.
547
442
  - \`<Stepper steps={STEPS} current={step} />\` — display-only step indicator for wizards/checkouts. \`steps\` is a top-level \`const\` array of labels; \`current\` is YOUR zero-indexed \`useState\` value; \`orientation\` is \`horizontal | vertical\`; optional \`onStepClick={(i) => …}\` makes steps clickable. Stepper never owns navigation state — your Next/Back handlers move \`current\`.
548
- - \`<Badge variant="...">\` — \`default | primary | secondary | success | warning | error | info\` for colored pills. Great for status/condition labels. There is NO \`neutral\` variant — use \`default\` (or \`secondary\`) for an un-tinted pill.
443
+ - \`<Badge variant="...">\` — \`default | primary | secondary | tertiary | success | warning | error | info\` for colored pills. Great for status/condition labels. There is NO \`neutral\` variant — use \`default\` (or \`secondary\`) for an un-tinted pill.
549
444
 
550
445
  **Color choice rule of thumb.** Reach for typed slots first: Button \`variant\`, Badge \`variant\`, Alert \`variant\`, Text/Heading/Icon/Spinner/Link/Divider \`tone\`, Box/Card \`surface\`. NEVER hardcode hex \`#XXXXXX\`, rgba, or hsl — tier-0 self-check rejects them with \`tokens:hex-color\` / \`tokens:hardcoded-color-fn\` and the LLM must remediate. Hardcoded colors break the operator's theme switch (Indigo → Claudic → Cyberpunk preset has zero effect on a card hardcoded with \`background: '#000'\`).
551
446
 
@@ -578,8 +473,8 @@ MANDATORY:
578
473
 
579
474
  Token categories:
580
475
  - Brand: \`var(--ggui-color-primary-600)\`, \`var(--ggui-color-primary-50)\`
581
- - Text: \`var(--ggui-color-onSurface)\`, \`var(--ggui-color-onSurfaceVariant)\`
582
- - Backgrounds: \`var(--ggui-color-surface)\`, \`var(--ggui-color-surfaceVariant)\`
476
+ - Text: \`var(--ggui-color-onContainer)\`, \`var(--ggui-color-onSunken)\`
477
+ - Backgrounds: \`var(--ggui-color-container)\`, \`var(--ggui-color-sunken)\`
583
478
  - Borders: \`var(--ggui-color-outline)\`
584
479
  - Spacing: \`var(--ggui-spacing-4)\`, \`var(--ggui-spacing-6)\`
585
480
  - Typography: \`var(--ggui-font-size-sm)\`, \`var(--ggui-font-weight-semibold)\`
@@ -600,7 +495,7 @@ Use the FULL primary palette throughout the component — NOT only on submit but
600
495
  | Buttons, CTAs, filled interactive elements | \`primary-600\` / \`primary-700\` | Primary actions |
601
496
  | Headings on light primary backgrounds | \`primary-800\` / \`primary-900\` | High-contrast branded text |
602
497
 
603
- Use semantic tokens (\`onSurface\`, \`onSurfaceVariant\`) for body text and secondary info. NEVER use raw \`neutral-*\` or \`gray-*\` for body text — they break in dark themes.
498
+ Use the layering roles' inks (\`onContainer\` for body text on a card, \`onSunken\` for secondary text, captions, labels) — never raw neutrals. NEVER use raw \`neutral-*\` or \`gray-*\` for body text — they break in dark themes.
604
499
 
605
500
  ### Theme-Agnostic Design
606
501
 
@@ -609,7 +504,7 @@ Components MUST be theme-agnostic — they reference CSS variables but NEVER ass
609
504
  DO:
610
505
  - Use \`var(--ggui-color-primary-*)\` for brand elements — the theme controls what "primary" means
611
506
  - Use \`var(--ggui-shape-shadow-*)\` for depth, \`var(--ggui-shape-radius-*)\` for corners
612
- - Use semantic color roles: primary for brand, surface/onSurface for structure, success/error/warning for state
507
+ - Use semantic color roles: primary for brand; the layering roles (ground / container / elevated / sunken + their on* inks) for structure; success/error/warning for state
613
508
 
614
509
  DON'T:
615
510
  - Don't assume primary is blue — could be red, green, purple
@@ -630,59 +525,9 @@ Generated components become reusable blueprints — the same blueprint serves ph
630
525
  2. Relative/fluid units — prefer \`%\`, \`em\`, \`rem\`, \`min()\`, \`max()\`, \`clamp()\` over fixed \`px\`.
631
526
  3. Fluid widths — \`max-width\` with \`width: 100%\`. Never set a fixed width.
632
527
  4. Compact padding — components are embedded in containers that provide their own chrome.
633
- 5. No raw \`@media\` queries in component code — for a layout that must change by breakpoint, use \`<Grid columns={{ base: 1, md: 3 }}>\` (the design system emits the media queries for you) or a fluid \`minColumnWidth\` grid.
634
-
635
- ## Data Parameterization (CRITICAL)
528
+ 5. No raw \`@media\` queries in component code — for a layout that must change by breakpoint, use \`<Grid columns={{ base: 1, md: 3 }}>\` (the design system emits the media queries for you) or a fluid \`minColumnWidth\` grid.`;
636
529
 
637
- 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:
638
-
639
- \`\`\`tsx
640
- // BAD — hardcoded, only works for Tokyo
641
- const city = "Tokyo";
642
- const temp = 18;
643
-
644
- // GOOD — parameterized via props with defaults from the request
645
- interface Props {
646
- city?: string;
647
- temperature?: number;
648
- }
649
- export default function WeatherCard({ city = "Tokyo", temperature = 18 }: Props) {
650
- // A controller can override for Seoul, Paris, etc.
651
- }
652
- \`\`\`
653
-
654
- Rules:
655
- 1. All request-specific data → props with defaults. City names, tickers, user names, dates, counts.
656
- 2. Layout and styling are universal. Colors, spacing, structure — these are the reusable part.
657
- 3. Default values come from the current request — so the component renders correctly standalone.
658
- 4. Props interface must be typed and exported.
659
-
660
- ## Component Structure
661
-
662
- 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.
663
-
664
- \`\`\`tsx
665
- import { useState } from 'react';
666
- import { Container, Card, Stack, Text, Button, Input } from '@ggui-ai/design';
667
-
668
- interface Props {
669
- onSubmit?: (data: unknown) => void;
670
- }
671
-
672
- function ItemCard({ item, onEdit }: { item: Item; onEdit: (id: string) => void }) {
673
- return <Card padding="md">…</Card>;
674
- }
675
-
676
- export default function GeneratedComponent({ onSubmit }: Props) {
677
- return (
678
- <Container>
679
- {items.map((item) => <ItemCard key={item.id} item={item} onEdit={handleEdit} />)}
680
- </Container>
681
- );
682
- }
683
- \`\`\`
684
-
685
- ## Aesthetic Guidance (READ CAREFULLY — this is what separates "polished" from "ok")
530
+ const DESIGN_SYSTEM_GUIDANCE_AESTHETICS_AND_CHECKLIST = `## Aesthetic Guidance (READ CAREFULLY — this is what separates "polished" from "ok")
686
531
 
687
532
  ### Visual hierarchy — the SCALE GAP rule
688
533
 
@@ -704,11 +549,11 @@ A polished UI has ONE hero that dominates. Everything else supports it. Bad layo
704
549
  ### Color discipline — the 60/30/10 rule
705
550
 
706
551
  Don't paint everything in primary. Use:
707
- - **60% surface** (\`var(--ggui-color-surface)\` / \`onSurface\`) — body text, default backgrounds, structure
708
- - **30% surfaceVariant + onSurfaceVariant** — secondary text, captions, labels, dividers
552
+ - **60% container** (\`var(--ggui-color-container)\` / \`onContainer\`) — body text, default backgrounds, structure
553
+ - **30% sunken + onSunken** — wells, secondary text, captions, labels, dividers
709
554
  - **10% primary** — hero number, ONE highlight element, CTAs, brand accent
710
555
 
711
- If your component is 100% purple text on purple backgrounds, you've lost the eye. Headings can be \`onSurface\` (dark neutral) — they'll still feel weighty. Save the primary palette for one or two STAR moments.
556
+ If your component is 100% purple text on purple backgrounds, you've lost the eye. Headings can be \`onContainer\` (the card's ink) — they'll still feel weighty. Save the primary palette for one or two STAR moments.
712
557
 
713
558
  \`\`\`tsx
714
559
  // BAD — everything purple, eye has no anchor
@@ -717,7 +562,7 @@ If your component is 100% purple text on purple backgrounds, you've lost the eye
717
562
  <Text tone="emphasized">all body text</Text>
718
563
 
719
564
  // GOOD — hero pops, body is neutral, primary is reserved
720
- <Heading>Title</Heading> {/* defaults to onSurface */}
565
+ <Heading>Title</Heading> {/* defaults to onContainer */}
721
566
  <Text size="4xl" weight="bold" tone="emphasized">42</Text>
722
567
  <Text tone="muted">all body text</Text>
723
568
  \`\`\`
@@ -775,3 +620,10 @@ Hero sections should feel airy. Use \`padding="xl"\` (32px) on the main card, no
775
620
  - [ ] Live/streaming regions wrapped in \`aria-live="polite"\`
776
621
  - [ ] Headings nest — one \`level={1}\`, then \`level={2}\`/\`{3}\` — never skipped or inverted
777
622
  - [ ] Wire hooks (\`useAction\`, \`useStream\`) imported from \`@ggui-ai/wire\` and consumed`;
623
+
624
+ export const DESIGN_SYSTEM_GUIDANCE = [
625
+ DESIGN_SYSTEM_GUIDANCE_SURFACE_AND_TOKENS,
626
+ DATA_PARAMETERIZATION,
627
+ COMPONENT_STRUCTURE,
628
+ DESIGN_SYSTEM_GUIDANCE_AESTHETICS_AND_CHECKLIST,
629
+ ].join("\n\n");
@@ -0,0 +1,24 @@
1
+ // DO NOT EDIT imports — the wire + gadget imports below are runtime-resolution anchors.
2
+ // prettier-ignore-start
3
+ /* eslint-disable no-unused-vars */
4
+ {{REACT_IMPORT}}
5
+ // Compose freely: raw HTML elements, inline `style` props and <style> blocks.
6
+ // The `@ggui-ai/design` package is importable but OPTIONAL — add your own
7
+ // import only when a primitive (Skeleton, EmptyState, Icon, …) saves you work.
8
+ // Color stays on bare var(--ggui-color-*) tokens; everything else may be literal.
9
+ {{WIRE_IMPORT}}/* eslint-enable no-unused-vars */
10
+ // prettier-ignore-end
11
+
12
+ // DO NOT EDIT Props — auto-generated from the data contract
13
+ {{PROPS_INTERFACE}}
14
+ {{ACTION_TYPES}}
15
+ {{STREAM_TYPES}}
16
+
17
+ // Place helper components here if needed
18
+
19
+ export default function Component(props: Props) {
20
+ {{CONTEXT_HOOKS}}{{WIRE_HOOKS}}{{AXIS_SECTIONS}}
21
+ return (
22
+ {{LAYOUT}}
23
+ );
24
+ }