@ggui-ai/ui-gen 0.5.0-rc.2 → 0.6.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 (54) hide show
  1. package/dist/adapters/base.js +1 -0
  2. package/dist/adapters/base.js.map +1 -1
  3. package/dist/adapters/generation-dispatch.js +60 -51
  4. package/dist/adapters/generation-dispatch.js.map +1 -1
  5. package/dist/adapters/index.js +13 -2
  6. package/dist/adapters/index.js.map +1 -1
  7. package/dist/advanced/index.js +61 -51
  8. package/dist/advanced/index.js.map +1 -1
  9. package/dist/blueprint-validator.js +1 -1
  10. package/dist/blueprint-validator.js.map +1 -1
  11. package/dist/boilerplate.js +13 -24
  12. package/dist/boilerplate.js.map +1 -1
  13. package/dist/check/index.d.ts +2 -2
  14. package/dist/check/index.js.map +1 -1
  15. package/dist/classifier/index.d.ts +14 -17
  16. package/dist/classifier/index.js +3 -7
  17. package/dist/classifier/index.js.map +1 -1
  18. package/dist/evaluation/index.js +12 -4
  19. package/dist/evaluation/index.js.map +1 -1
  20. package/dist/harness/check/runtime-render/index.js.map +1 -1
  21. package/dist/harness/index.js +25 -28
  22. package/dist/harness/index.js.map +1 -1
  23. package/dist/harness/runtime.js +14 -25
  24. package/dist/harness/runtime.js.map +1 -1
  25. package/dist/harness/types-public.js +13 -24
  26. package/dist/harness/types-public.js.map +1 -1
  27. package/dist/index.js +62 -51
  28. package/dist/index.js.map +1 -1
  29. package/dist/provider-adapter-contract.js +12 -3
  30. package/dist/provider-adapter-contract.js.map +1 -1
  31. package/dist/providers/index.d.ts +2 -2
  32. package/dist/providers/index.js.map +1 -1
  33. package/dist/validation/index.d.ts +1 -1
  34. package/dist/validation/index.js +5 -0
  35. package/dist/validation/index.js.map +1 -1
  36. package/package.json +12 -12
  37. package/src/blueprint-validator.ts +1 -1
  38. package/src/boilerplate/generate.ts +20 -31
  39. package/src/boilerplate/system-prompt.ts +2 -2
  40. package/src/check/extract-wire-calls.ts +2 -2
  41. package/src/classifier/axes.ts +2 -2
  42. package/src/classifier/classifier.ts +2 -2
  43. package/src/classifier/infer-layout.ts +1 -1
  44. package/src/classifier/infer-render.ts +1 -1
  45. package/src/classifier/infer-tooling.ts +1 -1
  46. package/src/classifier/infer-trigger.ts +3 -12
  47. package/src/classifier/inspect.ts +7 -7
  48. package/src/coding-agent/self-check.ts +1 -1
  49. package/src/evaluation/cost-tracker.ts +39 -11
  50. package/src/harness/check/runtime-render/render-check.ts +4 -4
  51. package/src/harness/llm-router.ts +2 -2
  52. package/src/providers/bedrock.ts +2 -2
  53. package/src/tools/get-wire.ts +1 -1
  54. package/src/validation/primitives.ts +2 -0
@@ -2,7 +2,7 @@ import { z } from 'zod';
2
2
  import * as esbuild2 from 'esbuild';
3
3
  import { build } from 'esbuild';
4
4
  import Anthropic from '@anthropic-ai/sdk';
5
- import { classifyUi, DANGEROUS_PATTERNS as DANGEROUS_PATTERNS$1, listContractGadgets, HOOK_NAME_RE, STDLIB_GADGETS_PACKAGE, resolveAppGadgets } from '@ggui-ai/protocol';
5
+ import { classifyUi, MODEL_REGISTRY, DANGEROUS_PATTERNS as DANGEROUS_PATTERNS$1, listContractGadgets, HOOK_NAME_RE, STDLIB_GADGETS_PACKAGE, resolveAppGadgets } from '@ggui-ai/protocol';
6
6
  import fs, { mkdirSync, writeFileSync, unlinkSync, readFileSync, existsSync, rm, createReadStream, createWriteStream } from 'fs';
7
7
  import { contentHash } from '@ggui-ai/protocol/content-hash';
8
8
  import ts7 from 'typescript';
@@ -32,11 +32,20 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
32
32
  if (typeof require !== "undefined") return require.apply(this, arguments);
33
33
  throw Error('Dynamic require of "' + x + '" is not supported');
34
34
  });
35
- var __esm = (fn, res) => function __init() {
36
- return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
35
+ var __esm = (fn, res, err) => function __init() {
36
+ if (err) throw err[0];
37
+ try {
38
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
39
+ } catch (e) {
40
+ throw err = [e], e;
41
+ }
37
42
  };
38
43
  var __commonJS = (cb, mod) => function __require2() {
39
- return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
44
+ try {
45
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
46
+ } catch (e) {
47
+ throw mod = 0, e;
48
+ }
40
49
  };
41
50
  var __export = (target, all) => {
42
51
  for (var name in all)
@@ -76,6 +85,7 @@ var init_base = __esm({
76
85
  constructor(config) {
77
86
  this.config = config;
78
87
  }
88
+ config;
79
89
  /**
80
90
  * Map a LiteLLM-format model ID to this SDK's native format.
81
91
  * Default: strips the provider prefix (e.g., 'anthropic/claude-sonnet-4-6' -> 'claude-sonnet-4-6').
@@ -1147,6 +1157,8 @@ var init_types = __esm({
1147
1157
  this.code = code;
1148
1158
  this.name = "OpenRouterError";
1149
1159
  }
1160
+ status;
1161
+ code;
1150
1162
  get isTransient() {
1151
1163
  return this.status === 408 || this.status === 429 || this.status === 502 || this.status === 503;
1152
1164
  }
@@ -1175,6 +1187,7 @@ var init_client2 = __esm({
1175
1187
  "X-Title": config.siteName ?? process.env.OPENROUTER_SITE_NAME ?? "ggui"
1176
1188
  };
1177
1189
  }
1190
+ config;
1178
1191
  baseUrl;
1179
1192
  headers;
1180
1193
  /**
@@ -1438,6 +1451,8 @@ var init_primitives = __esm({
1438
1451
  "EmptyState",
1439
1452
  "Stat",
1440
1453
  "Stepper",
1454
+ "Markdown",
1455
+ "MarkdownInline",
1441
1456
  "Header",
1442
1457
  "Sidebar",
1443
1458
  "CardGrid",
@@ -1884,6 +1899,7 @@ var init_ui_compiler = __esm({
1884
1899
  this.validation = validation;
1885
1900
  this.name = "UiValidationError";
1886
1901
  }
1902
+ validation;
1887
1903
  };
1888
1904
  UiBundleSizeError = class extends Error {
1889
1905
  constructor(size, limit) {
@@ -1892,6 +1908,8 @@ var init_ui_compiler = __esm({
1892
1908
  this.limit = limit;
1893
1909
  this.name = "UiBundleSizeError";
1894
1910
  }
1911
+ size;
1912
+ limit;
1895
1913
  };
1896
1914
  }
1897
1915
  });
@@ -4093,7 +4111,7 @@ function errorSummary(e) {
4093
4111
  }
4094
4112
  function interactionText(interaction) {
4095
4113
  let text = "";
4096
- for (const step of interaction.steps) {
4114
+ for (const step of interaction.steps ?? []) {
4097
4115
  if (step.type === "model_output") {
4098
4116
  for (const content of step.content ?? []) {
4099
4117
  if (content.type === "text") {
@@ -4105,7 +4123,7 @@ function interactionText(interaction) {
4105
4123
  return text;
4106
4124
  }
4107
4125
  function functionCallSteps(interaction) {
4108
- return interaction.steps.filter(
4126
+ return (interaction.steps ?? []).filter(
4109
4127
  (s) => s.type === "function_call"
4110
4128
  );
4111
4129
  }
@@ -6049,28 +6067,35 @@ var init_llm_evaluator = __esm({
6049
6067
  // src/evaluation/cost-tracker.ts
6050
6068
  var cost_tracker_exports = {};
6051
6069
  __export(cost_tracker_exports, {
6052
- CostTracker: () => CostTracker
6070
+ CostTracker: () => CostTracker,
6071
+ pricePer1k: () => pricePer1k
6053
6072
  });
6054
- var PRICE_PER_1K_TOKENS, CostTracker;
6073
+ function resolveRegistryId(model) {
6074
+ const keys = Object.keys(MODEL_REGISTRY);
6075
+ if (keys.includes(model)) return model;
6076
+ const bySuffix = keys.find((id) => id.endsWith(`/${model}`));
6077
+ if (bySuffix) return bySuffix;
6078
+ const undated = model.replace(/-\d{8}$/, "");
6079
+ return keys.find((id) => id.endsWith(`/${undated}`)) ?? null;
6080
+ }
6081
+ function pricePer1k(model) {
6082
+ const id = resolveRegistryId(model);
6083
+ if (id === null) return FALLBACK_PER_1K;
6084
+ const { costs } = MODEL_REGISTRY[id];
6085
+ return { input: costs.inputPer1M / 1e3, output: costs.outputPer1M / 1e3 };
6086
+ }
6087
+ var FALLBACK_PER_1K, CostTracker;
6055
6088
  var init_cost_tracker = __esm({
6056
6089
  "src/evaluation/cost-tracker.ts"() {
6057
- PRICE_PER_1K_TOKENS = {
6058
- "claude-sonnet-4-6": { input: 3e-3, output: 0.015 },
6059
- "claude-haiku-4-5-20251001": { input: 8e-4, output: 4e-3 },
6060
- "gemini-3-flash-preview": { input: 1e-4, output: 4e-4 },
6061
- // Gemini 3.5 Flash-Lite — $0.30/$2.50 per MTok (ai.google.dev/gemini-api/docs/pricing).
6062
- "gemini-3.5-flash-lite": { input: 3e-4, output: 25e-4 },
6063
- "gpt-5.4-mini": { input: 3e-4, output: 12e-4 },
6064
- // $1.00 in / $6.00 out per MTok (OpenAI list, 2026-07) → per-1K.
6065
- "gpt-5.6-luna": { input: 1e-3, output: 6e-3 }
6066
- };
6090
+ FALLBACK_PER_1K = { input: 3e-3, output: 0.015 };
6067
6091
  CostTracker = class {
6068
6092
  constructor(maxBudget) {
6069
6093
  this.maxBudget = maxBudget;
6070
6094
  }
6095
+ maxBudget;
6071
6096
  totalCost = 0;
6072
6097
  record(model, inputTokens, outputTokens) {
6073
- const prices = PRICE_PER_1K_TOKENS[model] ?? { input: 3e-3, output: 0.015 };
6098
+ const prices = pricePer1k(model);
6074
6099
  const cost = inputTokens / 1e3 * prices.input + outputTokens / 1e3 * prices.output;
6075
6100
  this.totalCost += cost;
6076
6101
  return cost;
@@ -12368,7 +12393,7 @@ function inspect(contract) {
12368
12393
  );
12369
12394
  actions.push({
12370
12395
  name,
12371
- tool: a?.tool,
12396
+ nextStep: a?.nextStep,
12372
12397
  example: a?.example,
12373
12398
  scalarKeys,
12374
12399
  allKeys,
@@ -12615,14 +12640,10 @@ function inferLayout(s, prompt, blueprint) {
12615
12640
  var DRAG_RX = /\b(drag|drop|dragging|draggable)\b/i;
12616
12641
  var SWIPE_RX = /\b(swipe|swipes|gesture\s*stack)\b/i;
12617
12642
  var KEYBOARD_RX = /\b(keyboard\s*shortcut|hotkey|keystroke|kbd)\b/i;
12618
- function inferWriteTrigger(s, prompt, blueprint) {
12643
+ function inferWriteTrigger(s, prompt) {
12619
12644
  if (s.actions.length === 0) {
12620
12645
  return { value: "click", source: "default" };
12621
12646
  }
12622
- if (blueprint?.mechanic === "drag")
12623
- return { value: "drag", source: "blueprint" };
12624
- if (blueprint?.mechanic === "swipe")
12625
- return { value: "swipe", source: "blueprint" };
12626
12647
  if (DRAG_RX.test(prompt)) return { value: "drag", source: "prompt" };
12627
12648
  if (SWIPE_RX.test(prompt)) return { value: "swipe", source: "prompt" };
12628
12649
  if (KEYBOARD_RX.test(prompt)) return { value: "keystroke", source: "prompt" };
@@ -12663,7 +12684,7 @@ function classifyAxes(input) {
12663
12684
  const fetch2 = inferFetch(s);
12664
12685
  const render = inferRender(s, prompt, blueprint);
12665
12686
  const layout = inferLayout(s, prompt, blueprint);
12666
- const writeTrigger = inferWriteTrigger(s, prompt, blueprint);
12687
+ const writeTrigger = inferWriteTrigger(s, prompt);
12667
12688
  const tooling = inferTooling(s);
12668
12689
  const vector = {
12669
12690
  render: render.value,
@@ -13485,28 +13506,17 @@ function inferTypeFromExample(example) {
13485
13506
  }
13486
13507
  function generateBoilerplate(_userPrompt, contract, shellType, screen, composedSections, appGadgets) {
13487
13508
  const propsFields = [];
13488
- const propsData = contract?.propsSpec;
13489
- const propsProperties = propsData?.properties ?? propsData ?? {};
13490
- for (const [key, value] of Object.entries(propsProperties)) {
13491
- if (typeof value === "object" && value !== null) {
13492
- const spec = value;
13493
- const schema = spec.schema;
13494
- const required = spec.required !== false;
13495
- const nullable = schema?.nullable === true;
13496
- const tsType = schema ? (
13497
- // JsonSchema extends JsonObject, so this is a plain structural
13498
- // downcast (no `unknown` erasure) of already-shaped contract data.
13499
- jsonSchemaTypeToTs(schema)
13500
- ) : "unknown";
13501
- const fullType = nullable ? `${tsType} | null` : tsType;
13502
- const parts = [];
13503
- if (spec.description) parts.push(String(spec.description));
13504
- if (spec.default !== void 0) parts.push(`(default: ${JSON.stringify(spec.default)})`);
13505
- const desc = parts.length > 0 ? ` // ${parts.join(" ")}` : "";
13506
- propsFields.push(` ${key}${required ? "" : "?"}: ${fullType};${desc}`);
13507
- } else {
13508
- propsFields.push(` ${key}: ${typeof value === "string" ? value : "unknown"};`);
13509
- }
13509
+ const propsProperties = contract?.propsSpec?.properties ?? {};
13510
+ for (const [key, entry] of Object.entries(propsProperties)) {
13511
+ const required = entry.required !== false;
13512
+ const nullable = entry.schema.nullable === true;
13513
+ const tsType = jsonSchemaTypeToTs(entry.schema);
13514
+ const fullType = nullable ? `${tsType} | null` : tsType;
13515
+ const parts = [];
13516
+ if (entry.description) parts.push(entry.description);
13517
+ if (entry.default !== void 0) parts.push(`(default: ${JSON.stringify(entry.default)})`);
13518
+ const desc = parts.length > 0 ? ` // ${parts.join(" ")}` : "";
13519
+ propsFields.push(` ${key}${required ? "" : "?"}: ${fullType};${desc}`);
13510
13520
  }
13511
13521
  const actionTypeAliases = [];
13512
13522
  const actionHookCalls = [];
@@ -14063,10 +14073,10 @@ A \`DataContract\` declares everything a GguiSession exchanges with the outside
14063
14073
 
14064
14074
  Props arrive via \`ggui_update\` and may be partial on first render. 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.**
14065
14075
 
14066
- - **Array iteration**: always default to \`[]\` before \`.map\`/\`.filter\`/\`.length\`. Use \`(props.items ?? []).map(...)\` not \`props.items.map(...)\`. Same for stream.history, stream.latest, etc.
14076
+ - **Array iteration**: always default to \`[]\` before \`.map\`/\`.filter\`/\`.length\`. Use \`(props.items ?? []).map(...)\` not \`props.items.map(...)\`. Same for stream.all, stream.latest, etc.
14067
14077
  - **Object access**: optional-chain through nested fields. \`props.user?.name ?? 'Anonymous'\` not \`props.user.name\`.
14068
14078
  - **Number ops**: default before arithmetic. \`(props.count ?? 0) + 1\` not \`props.count + 1\`.
14069
- - **Stream latest**: \`useStream\` returns \`{latest: T | undefined, history: T[]}\`. The default \`history\` is \`[]\` so it's safe to map; \`latest\` is undefined until the first frame arrives \u2014 guard before reading \`.foo\`.
14079
+ - **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 \u2014 guard before reading \`.foo\`.
14070
14080
  - **Stream reconciliation**: when a stream event carries an \`action\` discriminant (e.g. \`create | move | edit | delete\`), the channel is a CRUD feed \u2014 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 \u2014 and handle an event whose id is not yet present (a \`create\` for an unknown item) by inserting it, not ignoring it.
14071
14081
  - **Loading state**: while data is still absent, render \`<Skeleton>\` placeholders \u2014 never a blank screen. \`<Skeleton variant="text" />\` for a text line, \`variant="circle"\` for an avatar slot, default \`rect\` for a block.
14072
14082
  - **Empty state**: when a list or results array is empty, render \`<EmptyState title="\u2026" description="\u2026" />\` \u2014 a region that renders nothing when empty looks broken to the user.
@@ -14963,7 +14973,7 @@ init_policy2();
14963
14973
  init_validation();
14964
14974
 
14965
14975
  // src/tools/get-wire.ts
14966
- var 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 \u2014 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 \u2014 agentTools 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 \u2014 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 \u2014 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 \u2014 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** \u2014 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 \u2014 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'` \u2014 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 \u2014 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 \u2014 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 \u2014 it is provided by the renderer.\n\n### WireConfig\n\nConfiguration injected by the provider \u2014 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)` \u2014 `name` MUST be a declared actionSpec\n key; `data` MUST satisfy that action's schema.\n - `subscribe(channel, handler)` \u2014 same discipline for streamSpec.\n\nUntyped callers (`T = DataContract` default) degrade to the broad\nshape via the conditional `WireDispatchData` / `WireStreamPayload`\naliases \u2014 no call-site break.\n\nThe contract's `agentTools` 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 \u2014 `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 \u2014 the nested- interactive double-fire backstop. Reachability constraint: the first-party renderers do NOT set this field \u2014 `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";
14976
+ var 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 \u2014 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 \u2014 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 \u2014 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 \u2014 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 \u2014 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** \u2014 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 \u2014 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'` \u2014 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 \u2014 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 \u2014 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 \u2014 it is provided by the renderer.\n\n### WireConfig\n\nConfiguration injected by the provider \u2014 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)` \u2014 `name` MUST be a declared actionSpec\n key; `data` MUST satisfy that action's schema.\n - `subscribe(channel, handler)` \u2014 same discipline for streamSpec.\n\nUntyped callers (`T = DataContract` default) degrade to the broad\nshape via the conditional `WireDispatchData` / `WireStreamPayload`\naliases \u2014 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 \u2014 `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 \u2014 the nested- interactive double-fire backstop. Reachability constraint: the first-party renderers do NOT set this field \u2014 `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";
14967
14977
 
14968
14978
  // src/harness/pitfalls.ts
14969
14979
  var PITFALLS = [