@ggui-ai/ui-gen 0.18.0 → 0.19.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 (62) hide show
  1. package/dist/adapters/generation-dispatch.d.ts +1 -1
  2. package/dist/adapters/generation-dispatch.js +197 -86
  3. package/dist/adapters/generation-dispatch.js.map +1 -1
  4. package/dist/adapters/index.js +4 -6
  5. package/dist/adapters/index.js.map +1 -1
  6. package/dist/advanced/index.d.ts +2 -2
  7. package/dist/advanced/index.js +131 -22
  8. package/dist/advanced/index.js.map +1 -1
  9. package/dist/boilerplate.js +2 -2
  10. package/dist/boilerplate.js.map +1 -1
  11. package/dist/check/index.d.ts +1 -1
  12. package/dist/check/index.js.map +1 -1
  13. package/dist/{create-ui-generator-C8Ki9Wz1.d.ts → create-ui-generator-CxPvudbs.d.ts} +1 -1
  14. package/dist/{dispatch-D2htzi9h.d.ts → dispatch-N8wdNOX-.d.ts} +4 -2
  15. package/dist/evaluation/axis-checks/index.d.ts +3 -3
  16. package/dist/evaluation/axis-checks/index.js +97 -3
  17. package/dist/evaluation/axis-checks/index.js.map +1 -1
  18. package/dist/evaluation/axis-checks/registry.d.ts +1 -1
  19. package/dist/evaluation/axis-checks/registry.js +92 -1
  20. package/dist/evaluation/axis-checks/registry.js.map +1 -1
  21. package/dist/evaluation/index.d.ts +3 -3
  22. package/dist/evaluation/index.js +167 -84
  23. package/dist/evaluation/index.js.map +1 -1
  24. package/dist/harness/check/runtime-render/index.d.ts +1 -1
  25. package/dist/harness/index.d.ts +1 -1
  26. package/dist/harness/index.js +64 -61
  27. package/dist/harness/index.js.map +1 -1
  28. package/dist/harness/prompts.js +1 -1
  29. package/dist/harness/prompts.js.map +1 -1
  30. package/dist/harness/result-types.d.ts +1 -1
  31. package/dist/harness/runtime.d.ts +1 -1
  32. package/dist/harness/runtime.js +4 -4
  33. package/dist/harness/runtime.js.map +1 -1
  34. package/dist/harness/types-public.d.ts +8 -1
  35. package/dist/harness/types-public.js +64 -61
  36. package/dist/harness/types-public.js.map +1 -1
  37. package/dist/harness/validator-trace-sink.d.ts +1 -1
  38. package/dist/index.d.ts +2 -2
  39. package/dist/index.js +131 -22
  40. package/dist/index.js.map +1 -1
  41. package/dist/patch.d.ts +1 -1
  42. package/dist/{types-public-DVSssdc-.d.ts → types-public-DKR_h70H.d.ts} +9 -0
  43. package/dist/validation/index.d.ts +1 -1
  44. package/dist/validation/index.js +1 -1
  45. package/dist/validation/index.js.map +1 -1
  46. package/dist/workflows.d.ts +1 -1
  47. package/package.json +12 -12
  48. package/src/adapters/google/sdk.ts +11 -7
  49. package/src/boilerplate/hard-sections.ts +1 -1
  50. package/src/boilerplate/system-prompt.ts +1 -1
  51. package/src/coding-agent/tools.ts +58 -6
  52. package/src/contract-context.ts +1 -1
  53. package/src/create-harness.ts +1 -0
  54. package/src/evaluation/axis-checks/checks/universal.ts +115 -1
  55. package/src/evaluation/axis-checks/dispatch.ts +8 -1
  56. package/src/evaluation/types-public.ts +9 -0
  57. package/src/harness/coding/generate-task-runner.ts +1 -0
  58. package/src/harness/coding/run-coding-turn.ts +14 -0
  59. package/src/harness/types-public.ts +7 -0
  60. package/src/run-check.ts +2 -0
  61. package/src/tools/get-wire.ts +1 -1
  62. package/src/validation/primitives.ts +1 -1
@@ -2,7 +2,7 @@ import { C as Classification } from './axes-CzLEMDeB.js';
2
2
  import { Workflow } from './harness/types-public.js';
3
3
  import '@ggui-ai/protocol';
4
4
  import './styling-profile-CxfvV48g.js';
5
- import './types-public-DVSssdc-.js';
5
+ import './types-public-DKR_h70H.js';
6
6
  import './design-mode-DLufhrNp.js';
7
7
  import './types-BOvHNG7K.js';
8
8
  import './llm.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ggui-ai/ui-gen",
3
- "version": "0.18.0",
3
+ "version": "0.19.0",
4
4
  "description": "UI generation harness for ggui — the LLM system prompt, multi-turn coding loop, and evaluation engine that turn a DataContract into a React component. Implements the UiGenerator contract from @ggui-ai/mcp-server-core; consumed by @ggui-ai/mcp-server.",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [
@@ -188,9 +188,9 @@
188
188
  "puppeteer-core": "^24.40.0 || ^25.0.0",
189
189
  "react": "^18.0.0 || ^19.0.0",
190
190
  "react-dom": "^18.0.0 || ^19.0.0",
191
- "@ggui-ai/mcp-server-core": "0.18.0",
192
- "@ggui-ai/project-config": "0.18.0",
193
- "@ggui-ai/protocol": "0.18.0"
191
+ "@ggui-ai/mcp-server-core": "0.19.0",
192
+ "@ggui-ai/project-config": "0.19.0",
193
+ "@ggui-ai/protocol": "0.19.0"
194
194
  },
195
195
  "peerDependenciesMeta": {
196
196
  "@google/adk": {
@@ -229,11 +229,11 @@
229
229
  "openai": "^7.9.0",
230
230
  "typescript": "^5.0.0",
231
231
  "zod": "^4.3.6",
232
- "@ggui-ai/design": "0.18.0",
233
- "@ggui-ai/sandbox": "0.18.0",
234
- "@ggui-ai/ui-visual-tester": "0.18.0",
235
- "@ggui-ai/wire": "0.18.0",
236
- "@ggui-ai/gadgets": "0.18.0"
232
+ "@ggui-ai/design": "0.19.0",
233
+ "@ggui-ai/gadgets": "0.19.0",
234
+ "@ggui-ai/sandbox": "0.19.0",
235
+ "@ggui-ai/wire": "0.19.0",
236
+ "@ggui-ai/ui-visual-tester": "0.19.0"
237
237
  },
238
238
  "devDependencies": {
239
239
  "@google/adk": "^2.0.0",
@@ -248,9 +248,9 @@
248
248
  "react-dom": "^19.0.0",
249
249
  "tsup": "^8.5.0",
250
250
  "vitest": "^3.2.6",
251
- "@ggui-ai/project-config": "0.18.0",
252
- "@ggui-ai/protocol": "0.18.0",
253
- "@ggui-ai/mcp-server-core": "0.18.0"
251
+ "@ggui-ai/mcp-server-core": "0.19.0",
252
+ "@ggui-ai/project-config": "0.19.0",
253
+ "@ggui-ai/protocol": "0.19.0"
254
254
  },
255
255
  "engines": {
256
256
  "node": ">=22.0.0"
@@ -62,15 +62,10 @@ export async function runAdkLoop(
62
62
  ): Promise<AdapterResult> {
63
63
  const startTime = Date.now();
64
64
 
65
- const { FunctionTool, LlmAgent, InMemoryRunner, isFinalResponse } = await import('@google/adk');
65
+ const { FunctionTool, Gemini, LlmAgent, InMemoryRunner, isFinalResponse } = await import('@google/adk');
66
66
 
67
67
  const capture = createCapture();
68
68
 
69
- // Ensure ADK can find the API key
70
- if (params.apiKey && !process.env.GOOGLE_GENAI_API_KEY) {
71
- process.env.GOOGLE_GENAI_API_KEY = params.apiKey;
72
- }
73
-
74
69
  // Build tools with capture wrappers
75
70
  const adkTools = params.tools.map((def) =>
76
71
  new FunctionTool({
@@ -88,9 +83,18 @@ export async function runAdkLoop(
88
83
  }),
89
84
  );
90
85
 
86
+ // The key the caller passed WINS, handed to the model directly (ggui#1132).
87
+ // The ADK resolves GOOGLE_GENAI_API_KEY || GOOGLE_API_KEY || GEMINI_API_KEY
88
+ // on its own when no key is given, so writing GOOGLE_GENAI_API_KEY here —
89
+ // a variable this adapter does not own — let a stale ambient value beat a
90
+ // fresh key, and no rotation could reach it. Precedence is fixed at the
91
+ // source: an explicit key never touches process.env; no key defers to the
92
+ // ADK's order untouched.
93
+ const model = params.apiKey ? new Gemini({ model: params.model, apiKey: params.apiKey }) : params.model;
94
+
91
95
  const agent = new LlmAgent({
92
96
  name: 'ui_generator',
93
- model: params.model,
97
+ model,
94
98
  instruction: params.systemPrompt,
95
99
  tools: adkTools,
96
100
  });
@@ -213,7 +213,7 @@ Rules:
213
213
  1. All request-specific data → props with defaults. City names, tickers, user names, dates, counts.
214
214
  2. Layout and styling are universal. Colors, spacing, structure — these are the reusable part.
215
215
  3. Default values come from the current request — so the component renders correctly standalone.
216
- 4. Copy comes from the props. NEVER invent display text the contract does not carry — no eyebrow / kicker / overline (a \`<Text caps>\` label), no helper line, no status text, no tagline. A caps label exists only when a prop supplies its text (\`<Text caps>{props.section}</Text>\`); a heading's fallback is the request's own words, never a slogan of your own. Text the agent cannot change is chatter on every render.
216
+ 4. Copy comes from the props. NEVER invent display text the contract does not carry — no eyebrow / kicker / overline (a \`<Text caps>\` label), no helper line, no status text, no tagline. **Do NOT add a caps label above the heading** — not \`<Text caps>LET'S BEGIN</Text>\`, not \`WELCOME\`, not a section kicker like \`GETTING STARTED\` or \`OVERVIEW\` — UNLESS a prop supplies that exact text (\`<Text caps>{props.section}</Text>\`). When the request asks for a heading and a line, render the heading and the line and NOTHING above the heading. A heading's fallback is the request's own words, never a slogan of your own. Text the agent cannot change is chatter on every render, and a caps eyebrow you wrote is the most common form of it.
217
217
  5. Props interface must be typed and exported.`;
218
218
 
219
219
  /**
@@ -603,7 +603,7 @@ Don't render text-only metrics. A weather widget without a sun/cloud, a stock ca
603
603
  </Row>
604
604
  \`\`\`
605
605
 
606
- Use \`<Icon name="..." />\` for line icons — ONLY the curated Lucide subset renders (call \`get_available_icons\` for the exact kebab-case names; a name outside it renders an empty box and fails the check); emoji directly for status/weather/mood. Both are valid. For per-stat tiny accents, use a small icon next to the label.
606
+ Use \`<Icon name="..." />\` for line icons — ONLY the design system's curated set renders (~200 kebab-case Lucide names); a name OUTSIDE it renders an empty box and fails the check. On a single-pass render you cannot look the set up, so use ONLY a name you are sure is in it. Safe common names: \`check\` \`x\` \`plus\` \`minus\` \`search\` \`settings\` \`user\` \`users\` \`calendar\` \`clock\` \`arrow-right\` \`arrow-left\` \`chevron-right\` \`chevron-down\` \`trash-2\` \`edit\` \`info\` \`alert-circle\` \`bell\` \`home\` \`file\` \`file-text\` \`star\` \`heart\` \`mail\` \`send\` \`download\` \`upload\` \`external-link\` \`filter\` \`check-circle\`. The numbered variants (\`edit-2\`, \`edit-3\`) and synonyms (\`pencil\`, \`pen\`, \`trash\`, \`x-circle\`) are NOT in the set — use \`edit\` and \`trash-2\`. If the icon you want is not clearly one of these, OMIT it: a card with no icon beats one with an empty box. Emoji render directly for status/weather/mood. For per-stat tiny accents, use a small icon next to the label.
607
607
 
608
608
  ### Spacing — generosity beats compactness
609
609
 
@@ -12,6 +12,10 @@ import * as esbuild from 'esbuild';
12
12
  import { AgentWorkspace } from './workspace';
13
13
  import { getSoftWarnings } from './self-check';
14
14
  import { runTier0Checks } from '../check/index.js';
15
+ import { runGatedAxisChecks, traceAxisChecksSkipped } from '../evaluation/axis-checks/dispatch.js';
16
+ import type { AxisCheck, AxisCheckInput } from '../evaluation/types-public.js';
17
+ import type { Classification } from '../classifier/axes.js';
18
+ import type { CanvasClass } from '../design-mode.js';
15
19
  import { PRIMITIVES_DOCUMENTATION } from '../validation/index.js';
16
20
  import type { DataContract } from '@ggui-ai/protocol';
17
21
  import type {
@@ -46,6 +50,22 @@ function getComponentDocumentation(name: string): string {
46
50
  // Auto-commit helper (shared by write and apply_changes)
47
51
  // =============================================================================
48
52
 
53
+ /**
54
+ * The deterministic axis family at auto-commit (ggui#1122) — what a caller with a
55
+ * harness hands `executeTool` so the checks run on EVERY commit the coding agent
56
+ * makes, i.e. on the serve path, not only inside the eval round a serving deployment
57
+ * never enters. `checks` is the harness's pre-filtered list (`harness.check.axisChecks`)
58
+ * — the same list `run-check.ts` feeds the same pure runner. Absent: nothing runs.
59
+ */
60
+ export interface AutoCommitAxisChecks {
61
+ readonly checks: readonly AxisCheck[];
62
+ readonly classification: Classification;
63
+ /** The ORIGINAL request — several checks read it (labels invented past the copy, terminal words). */
64
+ readonly originalPrompt: string;
65
+ /** The canvas the harness was built for (ggui#1117); absent = no rendering context, never a default. */
66
+ readonly canvas?: CanvasClass;
67
+ }
68
+
49
69
  async function autoCommit(
50
70
  workspace: AgentWorkspace,
51
71
  commitMeta: Map<string, CommitMetadata>,
@@ -72,6 +92,8 @@ async function autoCommit(
72
92
  gadgetTypes?: Readonly<Record<string, string>>,
73
93
  /** Which tier-0 legs fire — see `runTier0Checks`'s `designMode`. */
74
94
  designMode: DesignMode = DEFAULT_DESIGN_MODE,
95
+ /** ggui#1122: run the harness's axis checks here too; absent, none run. */
96
+ axis?: AutoCommitAxisChecks,
75
97
  ): Promise<ToolResult> {
76
98
  const commitStart = Date.now();
77
99
  const raw = workspace.read();
@@ -123,17 +145,45 @@ async function autoCommit(
123
145
  gadgetTypes,
124
146
  designMode,
125
147
  );
126
- const tier0Fails = tier0Issues.filter(i => i.result === 'fail');
148
+ // ggui#1122: the deterministic axis family runs HERE — on every commit the coding
149
+ // agent makes, which is the serve path — through the one pure runner `run-check.ts`
150
+ // feeds (the ggui#1046 trace line prints on both paths). Posture is the founder's:
151
+ // warn-as-warn, fail-as-fail. A WARN that started blocking a served generation
152
+ // because it moved surface would be a posture change, not a relocation, and comes
153
+ // to him as a named item. Only a source that BUILT is judged: on a build failure
154
+ // the checks are not asked (the same stand-down the gate+run wrapper makes) and
155
+ // the ggui#1046 trace says so — a skipped round never reads as a clean one.
156
+ const axisFacts = axis !== undefined
157
+ ? {
158
+ sourceCode: formatted,
159
+ ...(contract !== undefined ? { contract } : {}),
160
+ originalPrompt: axis.originalPrompt,
161
+ classification: axis.classification,
162
+ designMode,
163
+ ...(axis.canvas !== undefined ? { canvas: axis.canvas } : {}),
164
+ }
165
+ : undefined;
166
+ if (axisFacts !== undefined && !buildSuccess) traceAxisChecksSkipped(axisFacts, 'build failed — no check ran');
167
+ const axisRun = axis !== undefined && axisFacts !== undefined && buildSuccess
168
+ ? runGatedAxisChecks(axis.checks, { ...axisFacts, compiledCode } satisfies AxisCheckInput)
169
+ : undefined;
170
+ const axisIssues = axisRun?.issues ?? [];
171
+ const checkIssues = [...tier0Issues, ...axisIssues];
172
+ const tier0Fails = checkIssues.filter(i => i.result === 'fail');
127
173
  const selfCheckPassed = tier0Fails.length === 0;
128
174
  // When contextPolicy.labeledTier0 is set, prefix each violation with
129
175
  // its P0/P1/P2 priority so the LLM can rank against the prompt's
130
176
  // schema. The default (off) emits unlabeled feedback.
131
- const violations = tier0Issues
177
+ const violations = checkIssues
132
178
  .filter(i => i.result === 'fail')
133
179
  .map(i => contextPolicy?.labeledTier0
134
180
  ? `[${i.priority ?? 'P0'}-${i.category}] ${i.description}\n Fix: ${i.fix}`
135
181
  : `[${i.category}] ${i.description}\n Fix: ${i.fix}`);
136
- const softWarnings = getSoftWarnings(raw);
182
+ // Axis WARNs ride the existing non-blocking channel — feedback the model sees, never a block.
183
+ const axisWarnings = axisIssues
184
+ .filter(i => i.result === 'warn')
185
+ .map(i => `[${i.subcategory}] ${i.description} Fix: ${i.fix}`);
186
+ const softWarnings = [...getSoftWarnings(raw), ...axisWarnings];
137
187
  const selfCheckMs = Date.now() - selfCheckStart;
138
188
 
139
189
  // Always commit (preserves history)
@@ -144,7 +194,7 @@ async function autoCommit(
144
194
 
145
195
  const status = buildSuccess && selfCheckPassed ? 'PASS' : 'FAIL';
146
196
  console.log(
147
- `[coding-agent] auto-commit: ${status} | build=${buildMs}ms self-check=${selfCheckMs}ms git=${gitMs}ms total=${Date.now() - commitStart}ms | violations=${violations.length}`,
197
+ `[coding-agent] auto-commit: ${status} | build=${buildMs}ms self-check=${selfCheckMs}ms git=${gitMs}ms total=${Date.now() - commitStart}ms | violations=${violations.length} | axis=${axisRun === undefined ? 'off' : `${axisIssues.length - axisWarnings.length}F/${axisWarnings.length}W`}`,
148
198
  );
149
199
  if (violations.length > 0) {
150
200
  for (const v of violations) {
@@ -202,6 +252,8 @@ export async function executeTool(
202
252
  gadgetTypes?: Readonly<Record<string, string>>,
203
253
  /** Which tier-0 legs fire on auto-commit — see `runTier0Checks`'s `designMode`. */
204
254
  designMode: DesignMode = DEFAULT_DESIGN_MODE,
255
+ /** ggui#1122: the harness's axis checks, run at every auto-commit — see `AutoCommitAxisChecks`. */
256
+ axis?: AutoCommitAxisChecks,
205
257
  ): Promise<ToolResult> {
206
258
  switch (tool) {
207
259
  case 'write':
@@ -222,7 +274,7 @@ export async function executeTool(
222
274
  const message = (input.commit_message as string) || `write ${lineCount} lines`;
223
275
 
224
276
  console.log(`[coding-agent] write: ${lineCount} lines → auto-commit`);
225
- return autoCommit(workspace, commitMeta, message, contract, contextPolicy, gadgetTypes, designMode);
277
+ return autoCommit(workspace, commitMeta, message, contract, contextPolicy, gadgetTypes, designMode, axis);
226
278
  }
227
279
 
228
280
  case 'apply_changes': {
@@ -529,7 +581,7 @@ export async function executeTool(
529
581
  const message = (input.commit_message as string) || 'apply changes';
530
582
 
531
583
  console.log(`[coding-agent] apply_changes: ${changes.length} changes applied → auto-commit`);
532
- return autoCommit(workspace, commitMeta, message, contract, contextPolicy, gadgetTypes, designMode);
584
+ return autoCommit(workspace, commitMeta, message, contract, contextPolicy, gadgetTypes, designMode, axis);
533
585
  }
534
586
 
535
587
  case 'get_components_info': {
@@ -213,7 +213,7 @@ export function buildContractsContext(
213
213
  // model reads it that way. Emitted only when true, so an absent flag
214
214
  // leaves the prompt bytes identical to before this line existed.
215
215
  if (entry.confirm === true) {
216
- line += `\n Author flag: \`confirm\` — the author marked this action grave enough to confirm before it fires, so a second press is a mistake.`;
216
+ line += `\n Author flag: \`confirm\` — the author marked this action as one to confirm with the user before it fires.`;
217
217
  }
218
218
  return line;
219
219
  })
@@ -224,6 +224,7 @@ export function createHarness(input: CreateHarnessInput): Harness {
224
224
  name,
225
225
  classification,
226
226
  designMode,
227
+ ...(input.canvas !== undefined ? { canvas: input.canvas } : {}),
227
228
  ...(input.profile !== undefined ? { profile: input.profile } : {}),
228
229
  how,
229
230
  what,
@@ -4,7 +4,8 @@
4
4
  // component. Dispatched via a "universal" gate that matches every render
5
5
  // value.
6
6
 
7
- import { LUCIDE_ICON_NAMES } from "@ggui-ai/design";
7
+ import { LUCIDE_ICON_NAMES, maxWidth as CONTAINER_MAX_WIDTH } from "@ggui-ai/design";
8
+ import { CANVAS_VIEWPORTS, type CanvasClass } from "../../../design-mode.js";
8
9
  import type { EvalIssue } from "../../types-public.js";
9
10
  import type { AxisCheck, AxisCheckInput } from "../types.js";
10
11
  import {
@@ -391,6 +392,113 @@ function runTerminalActionUnguarded(input: AxisCheckInput): EvalIssue[] {
391
392
  ];
392
393
  }
393
394
 
395
+ // ── universal.root_width_cap (ggui#1117 — the eval leg ggui#1113 left out) ─
396
+ // FRAME_SIZING's width half: the outermost element FILLS the frame; a width
397
+ // cap belongs INSIDE it, on the column that holds text. A cap on the root is
398
+ // the defect on a wide canvas (the card becomes a narrow strip with the
399
+ // frame's ground on either side) and a no-op in a chat bubble (~400 px; a
400
+ // 480 px cap never binds) — so the check reads `input.canvas`: it stands
401
+ // down with no canvas (never a guess) and below `md`, and fires only for a
402
+ // cap SMALLER than the canvas. A bare `<Container>` is a cap too — its
403
+ // default preset is `lg`, 768 px — so it binds on `lg` / `xl` and not on
404
+ // `md`. A cap the check cannot resolve to pixels (a `max-w-*` class, `rem`,
405
+ // `%`) is reported because the prompt forbids it on the root. WARN, not
406
+ // fail: the prompt names the one shape never to write; whether a centred
407
+ // column was the right composition stays the model's call.
408
+ export interface RootWidthCap {
409
+ /** The outermost element's tag name as written. */
410
+ readonly element: string;
411
+ /** The cap as written (`maxWidth="sm"`, `maxWidth: '480px'`, `max-w-md`, or `lg (Container default)`). */
412
+ readonly cap: string;
413
+ /** The cap in pixels when it resolves (a Container preset, an `Npx` value); `null` otherwise. */
414
+ readonly px: number | null;
415
+ }
416
+
417
+ /** The opening tag of the OUTERMOST element the default export returns, as written; `null` for none or a fragment. */
418
+ function rootOpeningTag(sourceCode: string): { readonly name: string; readonly attrs: string } | null {
419
+ const start = sourceCode.indexOf("export default function");
420
+ const body = start >= 0 ? sourceCode.slice(start) : sourceCode;
421
+ const ret = /\breturn\s*\(?\s*</.exec(body);
422
+ if (ret === null) return null;
423
+ const at = ret.index + ret[0].length - 1;
424
+ const nameMatch = /^<([A-Za-z][\w.]*)/.exec(body.slice(at));
425
+ if (nameMatch === null) return null;
426
+ let j = at + nameMatch[0].length;
427
+ let depth = 0;
428
+ let quote: string | null = null;
429
+ for (; j < body.length; j++) {
430
+ const ch = body[j]!;
431
+ if (quote !== null) {
432
+ if (ch === quote) quote = null;
433
+ continue;
434
+ }
435
+ if (ch === '"' || ch === "'" || ch === "`") quote = ch;
436
+ else if (ch === "{") depth++;
437
+ else if (ch === "}") depth--;
438
+ else if (ch === ">" && depth === 0) break;
439
+ }
440
+ return { name: nameMatch[1]!, attrs: body.slice(at + nameMatch[0].length, j) };
441
+ }
442
+
443
+ const CONTAINER_PRESETS: Readonly<Record<string, string>> = CONTAINER_MAX_WIDTH;
444
+ const NOT_A_CAP = new Set(["full", "none", "100%", "unset", "initial", "inherit"]);
445
+
446
+ /** `sm` → 480; `480px` → 480; `100%` / `full` → not a cap (`undefined`); anything else → a cap of unknown size (`null`). */
447
+ function capPx(value: string): number | null | undefined {
448
+ const v = value.trim();
449
+ if (NOT_A_CAP.has(v)) return undefined;
450
+ const resolved = CONTAINER_PRESETS[v] ?? v;
451
+ if (NOT_A_CAP.has(resolved)) return undefined;
452
+ const px = /^(\d+(?:\.\d+)?)px$/.exec(resolved);
453
+ return px === null ? null : Number(px[1]);
454
+ }
455
+
456
+ /** The width cap the outermost returned element carries, or `null` when it fills the frame. */
457
+ export function findRootWidthCap(sourceCode: string): RootWidthCap | null {
458
+ const root = rootOpeningTag(sourceCode);
459
+ if (root === null) return null;
460
+ const { name, attrs } = root;
461
+ const prop = /\bmaxWidth\s*=\s*(?:"([^"]*)"|'([^']*)'|\{\s*(?:"([^"]*)"|'([^']*)'|(\d+(?:\.\d+)?))\s*\})/.exec(attrs);
462
+ if (prop !== null) {
463
+ const raw = prop[1] ?? prop[2] ?? prop[3] ?? prop[4] ?? `${prop[5]!}px`;
464
+ const px = capPx(raw);
465
+ return px === undefined ? null : { element: name, cap: `maxWidth="${raw}"`, px };
466
+ }
467
+ const style = /\bmaxWidth\s*:\s*(?:"([^"]*)"|'([^']*)'|`([^`]*)`|(\d+(?:\.\d+)?))/.exec(attrs);
468
+ if (style !== null) {
469
+ const raw = style[1] ?? style[2] ?? style[3] ?? `${style[4]!}px`;
470
+ const px = capPx(raw);
471
+ return px === undefined ? null : { element: name, cap: `maxWidth: '${raw}'`, px };
472
+ }
473
+ const cls = /(?<![\w-])max-w-([\w[\]%.-]+)/.exec(attrs);
474
+ if (cls !== null) {
475
+ if (cls[1] === "full" || cls[1] === "none") return null;
476
+ return { element: name, cap: `max-w-${cls[1]!}`, px: null };
477
+ }
478
+ if (name === "Container") return { element: name, cap: "lg (Container default)", px: capPx("lg") ?? null };
479
+ return null;
480
+ }
481
+
482
+ const WIDE_CANVASES: ReadonlySet<CanvasClass> = new Set<CanvasClass>(["md", "lg", "xl"]);
483
+
484
+ function runRootWidthCap(input: AxisCheckInput): EvalIssue[] {
485
+ if (input.compiledCode === null) return [];
486
+ if (input.canvas === undefined || !WIDE_CANVASES.has(input.canvas)) return [];
487
+ const cap = findRootWidthCap(input.sourceCode);
488
+ if (cap === null) return [];
489
+ const width = CANVAS_VIEWPORTS[input.canvas].width;
490
+ if (cap.px !== null && cap.px >= width) return [];
491
+ const sized = cap.px !== null ? `${cap.px}px on a ${width}px canvas` : `on a ${width}px canvas`;
492
+ return [
493
+ mkIssue(
494
+ "universal.root_width_cap",
495
+ `The outermost element <${cap.element}> carries a width cap (${cap.cap}, ${sized}): on this canvas the card becomes a narrow strip with the frame's ground on either side. The frame owns the width; a cap belongs INSIDE the root, on the column that holds text.`,
496
+ 'Let the root fill the frame — drop the cap on the element you return at the top (or `maxWidth="full"`) — and put the reading measure on an inner column: `<Box padding="lg"><Container maxWidth="sm">…</Container></Box>`.',
497
+ "warn",
498
+ ),
499
+ ];
500
+ }
501
+
394
502
  export const UNIVERSAL_CHECKS: readonly AxisCheck[] = [
395
503
  {
396
504
  id: "universal.icon_name_known",
@@ -410,6 +518,12 @@ export const UNIVERSAL_CHECKS: readonly AxisCheck[] = [
410
518
  values: ALL_RENDER_VALUES,
411
519
  run: runViewportSized,
412
520
  },
521
+ {
522
+ id: "universal.root_width_cap",
523
+ axis: "render",
524
+ values: ALL_RENDER_VALUES,
525
+ run: runRootWidthCap,
526
+ },
413
527
  {
414
528
  id: "universal.terminal_action_unguarded",
415
529
  axis: "render",
@@ -9,7 +9,7 @@ import { REGISTRY } from "./registry.js";
9
9
  import { matches, type AxisCheck, type AxisCheckInput } from "./types.js";
10
10
  import { axisCheckTraceEnabled } from "./helpers.js";
11
11
  import { createHash } from "node:crypto";
12
- import type { DesignMode } from "../../design-mode.js";
12
+ import type { CanvasClass, DesignMode } from "../../design-mode.js";
13
13
 
14
14
  export interface RunAxisChecksInput {
15
15
  sourceCode: string;
@@ -18,6 +18,8 @@ export interface RunAxisChecksInput {
18
18
  originalPrompt: string;
19
19
  /** Which triad produced the source — see `AxisCheckInput.designMode`. */
20
20
  designMode?: DesignMode;
21
+ /** The canvas the source is judged for — see `AxisCheckInput.canvas` (ggui#1117); absent = no rendering context. */
22
+ canvas?: CanvasClass;
21
23
  }
22
24
 
23
25
  export function runAxisChecks(
@@ -30,6 +32,7 @@ export function runAxisChecks(
30
32
  originalPrompt: input.originalPrompt,
31
33
  classification,
32
34
  ...(input.designMode !== undefined ? { designMode: input.designMode } : {}),
35
+ ...(input.canvas !== undefined ? { canvas: input.canvas } : {}),
33
36
  };
34
37
  if (input.compiledCode === null) {
35
38
  traceAxisChecksSkipped(facts, "compiledCode null — no check ran");
@@ -93,6 +96,8 @@ export interface AxisCheckTrace {
93
96
  readonly classification: Classification["vector"];
94
97
  readonly propsSpecKeys: readonly string[];
95
98
  readonly propsSpecPropertyKeys: readonly string[];
99
+ /** The canvas the verdict was built for (ggui#1117) — present only when the input carried one. */
100
+ readonly canvas?: CanvasClass;
96
101
  readonly matched: readonly string[];
97
102
  readonly issues: ReadonlyArray<{ readonly id: string; readonly result: EvalIssue["result"] }>;
98
103
  readonly note?: string;
@@ -107,5 +112,7 @@ function traceFacts(input: AxisTraceFacts): Omit<AxisCheckTrace, "matched" | "is
107
112
  classification: input.classification.vector,
108
113
  propsSpecKeys: Object.keys(propsSpec ?? {}),
109
114
  propsSpecPropertyKeys: Object.keys(propsSpec?.properties ?? {}),
115
+ // No default here, unlike designMode: an absent canvas is a fact about the caller, not a guess to paper over.
116
+ ...(input.canvas !== undefined ? { canvas: input.canvas } : {}),
110
117
  };
111
118
  }
@@ -280,6 +280,15 @@ export interface AxisCheckInput {
280
280
  * means `constrained`.
281
281
  */
282
282
  designMode?: DesignMode;
283
+ /**
284
+ * The rendering canvas the source is judged FOR (ggui#1117). A width cap on
285
+ * the outermost element is a defect on a fullscreen canvas and a no-op in a
286
+ * chat bubble, so a check that reads width must know the surface. Populated
287
+ * where the harness is built (`Harness.canvas`); ABSENT for callers with no
288
+ * rendering context — a check handles `undefined`, it never assumes a
289
+ * default (a silent `lg` would be the same guess with a nicer face).
290
+ */
291
+ canvas?: CanvasClass;
283
292
  }
284
293
 
285
294
  /**
@@ -178,6 +178,7 @@ export function createGenerateTaskRunner(input: CreateGenerateRunnerInput): Task
178
178
  systemPrompt: session.systemPrompt,
179
179
  harness: session.harness,
180
180
  contract: params.contract,
181
+ originalPrompt: params.userPrompt,
181
182
  commitMeta: session.commitMeta,
182
183
  originalProps: session.originalProps,
183
184
  costTracker: session.costTracker,
@@ -155,6 +155,13 @@ export interface CodingTurnContext {
155
155
  readonly systemPrompt: string;
156
156
  readonly harness: Harness;
157
157
  readonly contract: DataContract | undefined;
158
+ /**
159
+ * The ORIGINAL request (ggui#1122) — what the eval round has always judged
160
+ * against, now handed to auto-commit so the axis checks run on the serve
161
+ * path with the same input. Required: a caller that forgot it would silently
162
+ * run no axis checks on every served generation.
163
+ */
164
+ readonly originalPrompt: string;
158
165
  readonly commitMeta: Map<string, CommitMetadata>;
159
166
  readonly originalProps: string | undefined;
160
167
  readonly costTracker: CostTracker | null;
@@ -669,6 +676,13 @@ ${closingInstruction}`;
669
676
  // Which tier-0 legs fire — the design-vocabulary legs stand down in
670
677
  // `free` mode; every contract leg runs in both.
671
678
  harness.designMode,
679
+ // ggui#1122: the harness's pre-filtered axis checks run at auto-commit — the serve path.
680
+ {
681
+ checks: harness.check.axisChecks,
682
+ classification: harness.classification,
683
+ originalPrompt: ctx.originalPrompt,
684
+ ...(harness.canvas !== undefined ? { canvas: harness.canvas } : {}),
685
+ },
672
686
  );
673
687
  const toolMs = Date.now() - toolStart;
674
688
 
@@ -303,6 +303,13 @@ export interface Harness {
303
303
  * into `free`.
304
304
  */
305
305
  readonly designMode: DesignMode;
306
+ /**
307
+ * The rendering canvas this harness was built for (ggui#1117) — threaded
308
+ * from `CreateHarnessInput.canvas` into the HOW leg (the `free` prompt
309
+ * states it) and, via `run-check.ts`, onto every axis check's input.
310
+ * ABSENT when the caller had no rendering context; never defaulted.
311
+ */
312
+ readonly canvas?: CanvasClass;
306
313
  /** The app's generation profile in force for this harness (#991), if any. */
307
314
  readonly profile?: GenerationProfileInput;
308
315
 
package/src/run-check.ts CHANGED
@@ -119,6 +119,8 @@ export async function runCheck(input: RunCheckInput): Promise<CheckResult> {
119
119
  originalPrompt: prompt,
120
120
  classification: harness.classification,
121
121
  designMode: harness.designMode,
122
+ // ggui#1117: the canvas the harness was built for, when it has one — never a default.
123
+ ...(harness.canvas !== undefined ? { canvas: harness.canvas } : {}),
122
124
  };
123
125
  // The one runner `runAxisChecks` shares (ggui#1046 trace prints here, on the served path).
124
126
  const axisRun = runGatedAxisChecks(check.axisChecks, axisInput);
@@ -2,4 +2,4 @@
2
2
  * AUTO-GENERATED from @ggui-ai/wire JSDoc.
3
3
  * Do not edit manually. Run: pnpm --filter @ggui-ai/wire generate:docs
4
4
  */
5
- export const WIRE_DOCUMENTATION = "# ggui Wire Hooks Reference\n\n> Wire hooks connect generated UI components to agent communication.\n> They are pre-imported in the boilerplate — use them directly.\n> All hooks must be called inside a GguiWireProvider (handled automatically by the renderer).\n\nImport: `import { useAction, useStream } from '@ggui-ai/wire'`\n\n## Communication Hooks\n\n\n\nThese are the wire primitives for component-agent communication. `useWiredTool` retired 2026-05-11 — agentCapabilities.tools is a catalog the AGENT invokes, not a component hook surface; user gestures use `useAction(name)` and the optional `nextStep` field on the action entry names the tool the agent SHOULD invoke next.\n\n### useAction\n\nFire an action to the agent. Fire-and-forget — no response, no pending state.\n\nEvery action is an event: it lands on the GguiSession's consume buffer and\nthe agent receives it on its next turn (via `ggui_consume`). The action\nentry's optional `nextStep` names the tool the agent SHOULD invoke in\nresponse — advisory only; the agent owns the call decision. Component code\nis identical either way: call `useAction(name)(payload)` and treat\n`nextStep` as informational (use it to inform button labels, icons, copy).\n\nRUNTIME DEDUP (backstop, not a feature). Same-`(name, payload)` calls within\none event-loop task are coalesced — the first wins, subsequent duplicates\nare suppressed. This is the structural defense against LLM-generated\nnested-interactive components where a Checkbox `onChange` and an outer\n`Card as={Clickable}` `onClick` both wire to the same `useAction` binding;\nthe inner gesture bubbles to the outer handler, so without dedup one user\nclick would fire the action twice (a toggle would run back-to-back and the\nuser's change would disappear). **Do not rely on this as a feature** — wire\neach `useAction` callback to exactly ONE interactive surface; the dedup\nexists only because the LLM's source code can be wrong in subtle ways and\nthe runtime is the only place that can see the actual event-bubble path.\nSee `dispatch-dedup.ts` for the full failure-mode rationale.\n\nNEVER SILENT. When the dedup fires, a `console.warn` is emitted in BOTH dev\nand prod with the full diagnostic. The suppression is always visible in\nbrowser DevTools; operators investigating a \"the second click does nothing\"\nreport see the warning immediately. A structured observer also exists as\n`WireConfig.onDispatchSuppressed`, but the first-party renderers never set\nit — it is reachable only from a hand-built `WireConfig` (see its\ndocstring), so the console signal is what operators get in practice.\n\n**Signature:** `useAction<T = unknown>(actionName: string): (data: T) => void`\n\n**Parameters:**\n\n| Param | Type | Description |\n|-------|------|-------------|\n| actionName | `string` | Action name from the action contract |\n\n**Returns:** `(data: T) => void`\n\n**Example:**\n```tsx\nconst submitForm = useAction<{name: string; email: string}>('formSubmit');\n\n// In JSX:\n<Button onClick={() => submitForm({ name, email })}>Submit</Button>\n```\n\n\n### useStream\n\nSubscribe to deliveries on a named stream channel.\n\nHonors the channel's per-delivery `mode` ('append' vs 'replace')\nand the optional `complete` terminal marker. Channels declared\n`mode: 'replace'` on the spec typically emit every delivery with\n`mode: 'replace'` — this hook folds them into a single-latest\nvalue without accumulating history.\n\n**Signature:** `useStream<T = unknown>(channelName: string): StreamResult<T>`\n\n**Parameters:**\n\n| Param | Type | Description |\n|-------|------|-------------|\n| channelName | `string` | Channel name from the GguiSession's streamSpec |\n\n**Returns:** `StreamResult<T>`\n\n| Property | Type | Description |\n|----------|------|-------------|\n| latest | `T \\| null` | Most recent payload delivered on this channel, or null if none received yet. |\n| all | `T[]` | All payloads accumulated on this channel. - `mode: 'append'` deliveries are pushed to the tail (continuous stream). - `mode: 'replace'` deliveries collapse `all` to a single-element array containing the latest payload — matching the channel's \"full replacement\" semantics. |\n| isComplete | `boolean` | Truthy after the channel has delivered an envelope with `complete: true`. Subscribers flip into a \"channel closed\" rendering state based on this signal; further deliveries on a completed channel are still accumulated, since the underlying wire doesn't enforce quiescence. |\n\n**Example:**\n```tsx\nconst progress = useStream<{ percent: number; message: string }>('progress');\n\n// In JSX:\n{progress.latest && (\n <Progress value={progress.latest.percent} />\n)}\n<Text>{progress.all.length} updates received</Text>\n```\n\n\n## Context Hooks\n\n\n\nRead-only access to render, app, and auth context.\n\n### useAuth\n\nRead-only auth context. Token excluded — auth is added server-side.\n\n**Signature:** `useAuth(): AuthInfo`\n\n**Returns:** `AuthInfo`\n\n| Property | Type | Description |\n|----------|------|-------------|\n| userId | `string` | userId |\n| isAuthenticated | `boolean` | isAuthenticated |\n\n**Example:**\n```tsx\nconst auth = useAuth();\n\n// In JSX:\n{auth.isAuthenticated\n ? <Text>Welcome, user {auth.userId}</Text>\n : <Text>Please sign in</Text>\n}\n```\n\n\n### useApp\n\nRead-only app metadata.\n\n**Signature:** `useApp(): AppInfo`\n\n**Returns:** `AppInfo`\n\n| Property | Type | Description |\n|----------|------|-------------|\n| appId | `string` | appId |\n| appName | `string` | appName |\n| appDescription | `string` | appDescription |\n| appIcon | `string` | appIcon |\n\n**Example:**\n```tsx\nconst app = useApp();\n\n// In JSX:\n<Heading>{app.appName}</Heading>\n{app.appDescription && <Text>{app.appDescription}</Text>}\n```\n\n\n### useRender\n\nRead-only render context with connection status.\n\n**Signature:** `useRender(): GguiSessionInfo`\n\n**Returns:** `GguiSessionInfo`\n\n| Property | Type | Description |\n|----------|------|-------------|\n| sessionId | `string` | sessionId |\n| isConnected | `boolean` | isConnected |\n\n**Example:**\n```tsx\nconst render = useRender();\n\n// In JSX:\n<Badge variant={render.isConnected ? 'success' : 'error'}>\n {render.isConnected ? 'Connected' : 'Disconnected'}\n</Badge>\n```\n\n\n## Provider\n\n\n\nThe provider is set up automatically by the renderer. Generated components do not need to wrap themselves in a provider.\n\n### GguiWireProvider\n\nReact context provider that injects WireConfig for all wire hooks.\n\n**Props:**\n\n| Prop | Type | Description |\n|------|------|-------------|\n| config | `WireConfig` | config |\n| children | `ReactNode` | children |\n\n**Example:**\n```tsx\n<GguiWireProvider config={wireConfig}>\n <YourComponent />\n</GguiWireProvider>\n```\n\n\n## Internal: WireConfig\n\n\n\nThis is the configuration object passed to GguiWireProvider. Generated components do not interact with this directly — it is provided by the renderer.\n\n### WireConfig\n\nConfiguration injected by the provider — the renderer inside the\niframe.\n\nEvery method is typed against the contract generic `T` so typed\ncallers get compile-time enforcement:\n - `dispatch(name, data)` — `name` MUST be a declared actionSpec\n key; `data` MUST satisfy that action's schema.\n - `subscribe(channel, handler)` — same discipline for streamSpec.\n\nUntyped callers (`T = DataContract` default) degrade to the broad\nshape via the conditional `WireDispatchData` / `WireStreamPayload`\naliases — no call-site break.\n\nThe contract's `agentCapabilities.tools` catalog declares tools the AGENT\ninvokes (not the component); user gestures fire via\n`dispatch(name, data)` and the optional `nextStep` field on the\naction entry names the tool the agent SHOULD invoke next.\n\nThe renderer mounts exactly ONE GguiSession per iframe — `render.sessionId`\nis the stable identity the WireProvider was constructed with. No\nper-item scoping factory; with a one-GguiSession-per-mount lifecycle,\n\"scope\" collapses to identity.\n\n| Property | Type | Description |\n|----------|------|-------------|\n| app | `{ readonly appId: string; readonly appName: string; readonly appDescription?: string; readonly appIcon?: string; }` | app |\n| render | `{ readonly sessionId: string; readonly isConnected: boolean; }` | render |\n| auth | `{ readonly userId?: string; readonly isAuthenticated: boolean; }` | auth |\n| dispatch | `<N extends string>( actionName: N, data: WireDispatchData<T, N>, ) => void` | Fire an action to the agent (fire-and-forget over WS). Typed callers get compile-time checked `name` + `data`; untyped callers (`T = DataContract`) keep the broad shape. |\n| subscribe | `<N extends string>( channelName: N, handler: (delivery: StreamDelivery<WireStreamPayload<T, N>>) => void, ) => () => void` | Subscribe to deliveries on a named stream channel. |\n| onDispatchSuppressed | `(info: DispatchSuppressedInfo) => void` | Optional structured observability for `useAction`'s task-scoped duplicate-dispatch suppression. Fires alongside the always-on `console.warn` whenever the runtime coalesces a same-(name, payload) re-dispatch within one event-loop task — the nested- interactive double-fire backstop. Reachability constraint: the first-party renderers do NOT set this field — `buildRootWireConfig` (`@ggui-ai/iframe-runtime`) and `<GguiRender>`'s internal config both omit it. It fires only when a host hand-builds a complete `WireConfig` and mounts `GguiWireProvider` itself. On every first-party render path the `console.warn` is the sole suppression signal. |\n";
5
+ export const WIRE_DOCUMENTATION = "# ggui Wire Hooks Reference\n\n> Wire hooks connect generated UI components to agent communication.\n> They are pre-imported in the boilerplate — use them directly.\n> All hooks must be called inside a GguiWireProvider (handled automatically by the renderer).\n\nImport: `import { useAction, useStream } from '@ggui-ai/wire'`\n\n## Communication Hooks\n\n\n\nThese are the wire primitives for component-agent communication. `useWiredTool` retired 2026-05-11 — agentCapabilities.tools is a catalog the AGENT invokes, not a component hook surface; user gestures use `useAction(name)` and the optional `nextStep` field on the action entry names the tool the agent SHOULD invoke next.\n\n### useAction\n\nFire an action to the agent. Fire-and-forget — no response, no pending state.\n\nEvery action is an event: it lands on the GguiSession's consume buffer and\nthe agent receives it on its next turn (via `ggui_consume`). The action\nentry's optional `nextStep` names the tool the agent SHOULD invoke in\nresponse — advisory only; the agent owns the call decision. Component code\nis identical either way: call `useAction(name)(payload)` and treat\n`nextStep` as informational (use it to inform button labels, icons, copy).\n\nRUNTIME DEDUP (backstop, not a feature). Same-`(name, payload)` calls within\none event-loop task are coalesced — the first wins, subsequent duplicates\nare suppressed. This is the structural defense against LLM-generated\nnested-interactive components where a Checkbox `onChange` and an outer\n`Card as={Clickable}` `onClick` both wire to the same `useAction` binding;\nthe inner gesture bubbles to the outer handler, so without dedup one user\nclick would fire the action twice (a toggle would run back-to-back and the\nuser's change would disappear). **Do not rely on this as a feature** — wire\neach `useAction` callback to exactly ONE interactive surface; the dedup\nexists only because the LLM's source code can be wrong in subtle ways and\nthe runtime is the only place that can see the actual event-bubble path.\nSee `dispatch-dedup.ts` for the full failure-mode rationale.\n\nNEVER SILENT. When the dedup fires, a `console.warn` is emitted in BOTH dev\nand prod with the full diagnostic. The suppression is always visible in\nbrowser DevTools; operators investigating a \"the second click does nothing\"\nreport see the warning immediately. A structured observer also exists as\n`WireConfig.onDispatchSuppressed`, but the first-party renderers never set\nit — it is reachable only from a hand-built `WireConfig` (see its\ndocstring), so the console signal is what operators get in practice.\n\n**Signature:** `useAction<T = unknown>(actionName: string): (data: T) => void`\n\n**Parameters:**\n\n| Param | Type | Description |\n|-------|------|-------------|\n| actionName | `string` | Action name from the action contract |\n\n**Returns:** `(data: T) => void`\n\n**Example:**\n```tsx\nconst submitForm = useAction<{name: string; email: string}>('formSubmit');\n\n// In JSX:\n<Button onClick={() => submitForm({ name, email })}>Submit</Button>\n```\n\n\n### useStream\n\nSubscribe to deliveries on a named stream channel.\n\nHonors the channel's per-delivery `mode` ('append' vs 'replace')\nand the optional `complete` terminal marker. Channels declared\n`mode: 'replace'` on the spec typically emit every delivery with\n`mode: 'replace'` — this hook folds them into a single-latest\nvalue without accumulating history.\n\n**Signature:** `useStream<T = unknown>(channelName: string): StreamResult<T>`\n\n**Parameters:**\n\n| Param | Type | Description |\n|-------|------|-------------|\n| channelName | `string` | Channel name from the GguiSession's streamSpec |\n\n**Returns:** `StreamResult<T>`\n\n| Property | Type | Description |\n|----------|------|-------------|\n| latest | `T \\| null` | Most recent payload delivered on this channel, or null if none received yet. |\n| all | `T[]` | All payloads accumulated on this channel. - `mode: 'append'` deliveries are pushed to the tail (continuous stream). - `mode: 'replace'` deliveries collapse `all` to a single-element array containing the latest payload — matching the channel's \"full replacement\" semantics. |\n| isComplete | `boolean` | Truthy after the channel has delivered an envelope with `complete: true`. Subscribers flip into a \"channel closed\" rendering state based on this signal; further deliveries on a completed channel are still accumulated, since the underlying wire doesn't enforce quiescence. |\n\n**Example:**\n```tsx\nconst progress = useStream<{ percent: number; message: string }>('progress');\n\n// In JSX:\n{progress.latest && (\n <Progress value={progress.latest.percent} />\n)}\n<Text>{progress.all.length} updates received</Text>\n```\n\n\n## Context Hooks\n\n\n\nRead-only access to render, app, and auth context.\n\n### useAuth\n\nRead-only auth context. Token excluded — auth is added server-side.\n\n**Signature:** `useAuth(): AuthInfo`\n\n**Returns:** `AuthInfo`\n\n| Property | Type | Description |\n|----------|------|-------------|\n| userId | `string` | userId |\n| isAuthenticated | `boolean` | isAuthenticated |\n\n**Example:**\n```tsx\nconst auth = useAuth();\n\n// In JSX:\n{auth.isAuthenticated\n ? <Text>Welcome, user {auth.userId}</Text>\n : <Text>Please sign in</Text>\n}\n```\n\n\n### useApp\n\nRead-only app metadata.\n\n**Signature:** `useApp(): AppInfo`\n\n**Returns:** `AppInfo`\n\n| Property | Type | Description |\n|----------|------|-------------|\n| appId | `string` | appId |\n| appName | `string` | appName |\n| appDescription | `string` | appDescription |\n| appIcon | `string` | appIcon |\n\n**Example:**\n```tsx\nconst app = useApp();\n\n// In JSX:\n<Heading>{app.appName}</Heading>\n{app.appDescription && <Text>{app.appDescription}</Text>}\n```\n\n\n### useRender\n\nRead-only render context with connection status.\n\n**Signature:** `useRender(): GguiSessionInfo`\n\n**Returns:** `GguiSessionInfo`\n\n| Property | Type | Description |\n|----------|------|-------------|\n| sessionId | `string` | sessionId |\n| isConnected | `boolean` | isConnected |\n\n**Example:**\n```tsx\nconst render = useRender();\n\n// In JSX:\n<Badge variant={render.isConnected ? 'success' : 'error'}>\n {render.isConnected ? 'Connected' : 'Disconnected'}\n</Badge>\n```\n\n\n## Provider\n\n\n\nThe provider is set up automatically by the renderer. Generated components do not need to wrap themselves in a provider.\n\n### GguiWireProvider\n\nReact context provider that injects WireConfig for all wire hooks.\n\n**Props:**\n\n| Prop | Type | Description |\n|------|------|-------------|\n| config | `WireConfig` | config |\n| children | `ReactNode` | children |\n\n**Example:**\n```tsx\n<GguiWireProvider config={wireConfig}>\n <YourComponent />\n</GguiWireProvider>\n```\n\n\n## Internal: WireConfig\n\n\n\nThis is the configuration object passed to GguiWireProvider. Generated components do not interact with this directly — it is provided by the renderer.\n\n### WireConfig\n\nConfiguration injected by the provider — the renderer inside the\niframe.\n\nEvery method is typed against the contract generic `T` so typed\ncallers get compile-time enforcement:\n - `dispatch(name, data)` — `name` MUST be a declared actionSpec\n key; `data` MUST satisfy that action's schema.\n - `subscribe(channel, handler)` — same discipline for streamSpec.\n\nUntyped callers (`T = DataContract` default) degrade to the broad\nshape via the conditional `WireDispatchData` / `WireStreamPayload`\naliases — no call-site break.\n\nThe contract's `agentCapabilities.tools` catalog declares tools the AGENT\ninvokes (not the component); user gestures fire via\n`dispatch(name, data)` and the optional `nextStep` field on the\naction entry names the tool the agent SHOULD invoke next.\n\nThe renderer mounts exactly ONE GguiSession per iframe — `render.sessionId`\nis the stable identity the WireProvider was constructed with. No\nper-item scoping factory; with a one-GguiSession-per-mount lifecycle,\n\"scope\" collapses to identity.\n\n| Property | Type | Description |\n|----------|------|-------------|\n| app | `{ readonly appId: string; readonly appName: string; readonly appDescription?: string; readonly appIcon?: string; }` | app |\n| render | `{ readonly sessionId: string; readonly isConnected: boolean; }` | render |\n| auth | `{ readonly userId?: string; readonly isAuthenticated: boolean; }` | auth |\n| dispatch | `<N extends string>( actionName: N, data: WireDispatchData<T, N>, ) => void` | Fire an action to the agent (fire-and-forget over WS). Typed callers get compile-time checked `name` + `data`; untyped callers (`T = DataContract`) keep the broad shape. |\n| subscribe | `<N extends string>( channelName: N, handler: (delivery: StreamDelivery<WireStreamPayload<T, N>>) => void, ) => () => void` | Subscribe to deliveries on a named stream channel. |\n| onDispatchSuppressed | `(info: DispatchSuppressedInfo) => void` | Optional structured observability for the runtime's two dispatch- suppression invariants — the task-scoped duplicate backstop (`reason: 'duplicate-dispatch'`, fired by `useAction`) and the render-lifetime one-shot guard (`reason: 'one-shot-spent'`, fired by the config's own `dispatch` when a declared `oneShot` action is gestured a second time, ggui#1108). Both fire ALONGSIDE the always-on `console.warn`; neither reaches the agent. Reachability constraint: the first-party renderers do NOT set this field — `buildRootWireConfig` (`@ggui-ai/iframe-runtime`) and `<GguiRender>`'s internal config both omit it. It fires only when a host hand-builds a complete `WireConfig` (or passes `onDispatchSuppressed` to `buildWireConfig`). On every first-party render path the `console.warn` is the sole suppression signal — which is what the `oneShot` contract requires as its minimum trace. |\n";