@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
@@ -3,6 +3,7 @@ var GeneratorAdapter = class {
3
3
  constructor(config) {
4
4
  this.config = config;
5
5
  }
6
+ config;
6
7
  /**
7
8
  * Map a LiteLLM-format model ID to this SDK's native format.
8
9
  * Default: strips the provider prefix (e.g., 'anthropic/claude-sonnet-4-6' -> 'claude-sonnet-4-6').
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/adapters/base.ts"],"names":[],"mappings":";AAoFO,IAAe,mBAAf,MAAgC;AAAA,EAKrC,YAA+B,MAAA,EAA0B;AAA1B,IAAA,IAAA,CAAA,MAAA,GAAA,MAAA;AAAA,EAA2B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAiB1D,eAAe,cAAA,EAAgC;AAC7C,IAAA,OAAO,iBAAiB,cAAc,CAAA;AAAA,EACxC;AACF;AAQA,SAAS,iBAAiB,OAAA,EAAyB;AACjD,EAAA,MAAM,UAAA,GAAa,OAAA,CAAQ,OAAA,CAAQ,GAAG,CAAA;AACtC,EAAA,OAAO,cAAc,CAAA,GAAI,OAAA,CAAQ,KAAA,CAAM,UAAA,GAAa,CAAC,CAAA,GAAI,OAAA;AAC3D;AAKO,SAAS,cAAA,CACd,WACG,WAAA,EACM;AACT,EAAA,IAAI,MAAA,CAAO,QAAQ,OAAO,IAAA;AAC1B,EAAA,IAAI,MAAA,CAAO,YAAY,OAAO,IAAA;AAC9B,EAAA,OAAO,WAAA,CAAY,KAAK,CAAC,IAAA,KAAS,CAAC,CAAC,OAAA,CAAQ,GAAA,CAAI,IAAI,CAAC,CAAA;AACvD","file":"base.js","sourcesContent":["// packages/ui-gen/src/adapters/base.ts\n//\n// Abstract base class for all generator adapters.\n\nimport type { McpServerConfig, AgentDefinition } from '@anthropic-ai/claude-agent-sdk';\nimport type { JsonObject } from '@ggui-ai/protocol';\nimport type { AdapterResult, ToolDefinition, ProviderName, AdapterMode } from './types';\n\n/**\n * Core configuration shared by all adapters.\n */\nexport interface AdapterConfig {\n /** API key for the provider (BYOK) */\n apiKey?: string;\n /** Use Bedrock instead of direct API (Claude only) */\n useBedrock?: boolean;\n /**\n * Thinking level for models with extended thinking (Gemini 3.x Flash\n * via the Interactions API). Mirrors the API's `thinking_level` enum —\n * `'minimal'` is the floor (the API has NO true off), then `'low'` /\n * `'medium'` / `'high'`. Omitted → provider default, and Gemini 3.5\n * Flash defaults thinking ON with no cap — the dominant latency cost\n * in contract-bounded UI generation. Only the Google adapter consumes\n * this today.\n */\n thinkingLevel?: 'minimal' | 'low' | 'medium' | 'high';\n}\n\n/**\n * Extended configuration for Claude Agent SDK adapter.\n * Separated from AdapterConfig to avoid polluting the base interface\n * with provider-specific concerns.\n */\nexport interface ClaudeSdkConfig extends AdapterConfig {\n /** Working directory for SDK subprocess */\n cwd?: string;\n /** Environment variables for SDK subprocess */\n env?: Record<string, string>;\n /** MCP server configs for Claude Agent SDK (keyed by name) */\n mcpServers?: Record<string, McpServerConfig>;\n /** Allowed tool names for Claude Agent SDK */\n allowedTools?: string[];\n /** Subagent definitions for Claude Agent SDK */\n agents?: Record<string, AgentDefinition>;\n /** Custom CLI path for Docker deployments */\n cliPath?: string;\n /** stderr callback for debug logging */\n stderr?: (data: string) => void;\n /** Progress callback */\n onProgress?: (event: { type: string; [key: string]: unknown }) => void;\n}\n\n/**\n * Union config type — adapters accept their specific config\n * but the registry uses this union so callers can pass any config.\n */\nexport type AnyAdapterConfig = AdapterConfig | ClaudeSdkConfig;\n\n/**\n * Parameters for a single generation run.\n */\nexport interface GenerateParams {\n /** System prompt (HOW to build — shared across all SDKs) */\n systemPrompt: string;\n /** User prompt (WHAT to build — the specific request) */\n userPrompt: string;\n /** Model identifier (provider-native format, e.g., 'claude-sonnet-4-6') */\n model: string;\n /** SDK-agnostic tool definitions */\n tools: ToolDefinition[];\n /** Maximum agentic loop turns */\n maxTurns: number;\n /** Model-specific options (e.g., speed_priority for Claude fast mode) */\n modelOptions?: JsonObject;\n}\n\n/**\n * Abstract base class for generator adapters.\n *\n * Each adapter wraps a specific provider SDK (Claude, OpenAI, Google)\n * in either raw API mode or agent SDK mode, and implements the tool-use\n * loop using the SDK's native API. All adapters produce the same\n * output format (AdapterResult).\n */\nexport abstract class GeneratorAdapter {\n abstract readonly provider: ProviderName;\n abstract readonly mode: AdapterMode;\n abstract readonly displayName: string;\n\n constructor(protected readonly config: AnyAdapterConfig) {}\n\n /**\n * Run UI generation with the given tools, prompt, and model.\n */\n abstract generate(params: GenerateParams): Promise<AdapterResult>;\n\n /**\n * Check if this adapter can run (SDK installed, credentials available).\n */\n abstract isAvailable(): boolean;\n\n /**\n * Map a LiteLLM-format model ID to this SDK's native format.\n * Default: strips the provider prefix (e.g., 'anthropic/claude-sonnet-4-6' -> 'claude-sonnet-4-6').\n * Override only if the prefix convention differs.\n */\n resolveModelId(litellmModelId: string): string {\n return stripModelPrefix(litellmModelId);\n }\n}\n\n/**\n * Strip provider prefix from LiteLLM-format model IDs.\n * 'anthropic/claude-sonnet-4-6' -> 'claude-sonnet-4-6'\n * 'openai/gpt-5.3-codex' -> 'gpt-5.3-codex'\n * 'gemini/gemini-3-flash-preview' -> 'gemini-3-flash-preview'\n */\nfunction stripModelPrefix(modelId: string): string {\n const slashIndex = modelId.indexOf('/');\n return slashIndex >= 0 ? modelId.slice(slashIndex + 1) : modelId;\n}\n\n/**\n * Check if credentials are available from config or environment variables.\n */\nexport function hasCredentials(\n config: AdapterConfig,\n ...envVarNames: string[]\n): boolean {\n if (config.apiKey) return true;\n if (config.useBedrock) return true;\n return envVarNames.some((name) => !!process.env[name]);\n}\n"]}
1
+ {"version":3,"sources":["../../src/adapters/base.ts"],"names":[],"mappings":";AAoFO,IAAe,mBAAf,MAAgC;AAAA,EAKrC,YAA+B,MAAA,EAA0B;AAA1B,IAAA,IAAA,CAAA,MAAA,GAAA,MAAA;AAAA,EAA2B;AAAA,EAA3B,MAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAiB/B,eAAe,cAAA,EAAgC;AAC7C,IAAA,OAAO,iBAAiB,cAAc,CAAA;AAAA,EACxC;AACF;AAQA,SAAS,iBAAiB,OAAA,EAAyB;AACjD,EAAA,MAAM,UAAA,GAAa,OAAA,CAAQ,OAAA,CAAQ,GAAG,CAAA;AACtC,EAAA,OAAO,cAAc,CAAA,GAAI,OAAA,CAAQ,KAAA,CAAM,UAAA,GAAa,CAAC,CAAA,GAAI,OAAA;AAC3D;AAKO,SAAS,cAAA,CACd,WACG,WAAA,EACM;AACT,EAAA,IAAI,MAAA,CAAO,QAAQ,OAAO,IAAA;AAC1B,EAAA,IAAI,MAAA,CAAO,YAAY,OAAO,IAAA;AAC9B,EAAA,OAAO,WAAA,CAAY,KAAK,CAAC,IAAA,KAAS,CAAC,CAAC,OAAA,CAAQ,GAAA,CAAI,IAAI,CAAC,CAAA;AACvD","file":"base.js","sourcesContent":["// packages/ui-gen/src/adapters/base.ts\n//\n// Abstract base class for all generator adapters.\n\nimport type { McpServerConfig, AgentDefinition } from '@anthropic-ai/claude-agent-sdk';\nimport type { JsonObject } from '@ggui-ai/protocol';\nimport type { AdapterResult, ToolDefinition, ProviderName, AdapterMode } from './types';\n\n/**\n * Core configuration shared by all adapters.\n */\nexport interface AdapterConfig {\n /** API key for the provider (BYOK) */\n apiKey?: string;\n /** Use Bedrock instead of direct API (Claude only) */\n useBedrock?: boolean;\n /**\n * Thinking level for models with extended thinking (Gemini 3.x Flash\n * via the Interactions API). Mirrors the API's `thinking_level` enum —\n * `'minimal'` is the floor (the API has NO true off), then `'low'` /\n * `'medium'` / `'high'`. Omitted → provider default, and Gemini 3.5\n * Flash defaults thinking ON with no cap — the dominant latency cost\n * in contract-bounded UI generation. Only the Google adapter consumes\n * this today.\n */\n thinkingLevel?: 'minimal' | 'low' | 'medium' | 'high';\n}\n\n/**\n * Extended configuration for Claude Agent SDK adapter.\n * Separated from AdapterConfig to avoid polluting the base interface\n * with provider-specific concerns.\n */\nexport interface ClaudeSdkConfig extends AdapterConfig {\n /** Working directory for SDK subprocess */\n cwd?: string;\n /** Environment variables for SDK subprocess */\n env?: Record<string, string>;\n /** MCP server configs for Claude Agent SDK (keyed by name) */\n mcpServers?: Record<string, McpServerConfig>;\n /** Allowed tool names for Claude Agent SDK */\n allowedTools?: string[];\n /** Subagent definitions for Claude Agent SDK */\n agents?: Record<string, AgentDefinition>;\n /** Custom CLI path for Docker deployments */\n cliPath?: string;\n /** stderr callback for debug logging */\n stderr?: (data: string) => void;\n /** Progress callback */\n onProgress?: (event: { type: string; [key: string]: unknown }) => void;\n}\n\n/**\n * Union config type — adapters accept their specific config\n * but the registry uses this union so callers can pass any config.\n */\nexport type AnyAdapterConfig = AdapterConfig | ClaudeSdkConfig;\n\n/**\n * Parameters for a single generation run.\n */\nexport interface GenerateParams {\n /** System prompt (HOW to build — shared across all SDKs) */\n systemPrompt: string;\n /** User prompt (WHAT to build — the specific request) */\n userPrompt: string;\n /** Model identifier (provider-native format, e.g., 'claude-sonnet-4-6') */\n model: string;\n /** SDK-agnostic tool definitions */\n tools: ToolDefinition[];\n /** Maximum agentic loop turns */\n maxTurns: number;\n /** Model-specific options (e.g., speed_priority for Claude fast mode) */\n modelOptions?: JsonObject;\n}\n\n/**\n * Abstract base class for generator adapters.\n *\n * Each adapter wraps a specific provider SDK (Claude, OpenAI, Google)\n * in either raw API mode or agent SDK mode, and implements the tool-use\n * loop using the SDK's native API. All adapters produce the same\n * output format (AdapterResult).\n */\nexport abstract class GeneratorAdapter {\n abstract readonly provider: ProviderName;\n abstract readonly mode: AdapterMode;\n abstract readonly displayName: string;\n\n constructor(protected readonly config: AnyAdapterConfig) {}\n\n /**\n * Run UI generation with the given tools, prompt, and model.\n */\n abstract generate(params: GenerateParams): Promise<AdapterResult>;\n\n /**\n * Check if this adapter can run (SDK installed, credentials available).\n */\n abstract isAvailable(): boolean;\n\n /**\n * Map a LiteLLM-format model ID to this SDK's native format.\n * Default: strips the provider prefix (e.g., 'anthropic/claude-sonnet-4-6' -> 'claude-sonnet-4-6').\n * Override only if the prefix convention differs.\n */\n resolveModelId(litellmModelId: string): string {\n return stripModelPrefix(litellmModelId);\n }\n}\n\n/**\n * Strip provider prefix from LiteLLM-format model IDs.\n * 'anthropic/claude-sonnet-4-6' -> 'claude-sonnet-4-6'\n * 'openai/gpt-5.3-codex' -> 'gpt-5.3-codex'\n * 'gemini/gemini-3-flash-preview' -> 'gemini-3-flash-preview'\n */\nfunction stripModelPrefix(modelId: string): string {\n const slashIndex = modelId.indexOf('/');\n return slashIndex >= 0 ? modelId.slice(slashIndex + 1) : modelId;\n}\n\n/**\n * Check if credentials are available from config or environment variables.\n */\nexport function hasCredentials(\n config: AdapterConfig,\n ...envVarNames: string[]\n): boolean {\n if (config.apiKey) return true;\n if (config.useBedrock) return true;\n return envVarNames.some((name) => !!process.env[name]);\n}\n"]}
@@ -1,6 +1,6 @@
1
1
  import Anthropic from '@anthropic-ai/sdk';
2
2
  import 'zod';
3
- import { classifyUi, DANGEROUS_PATTERNS as DANGEROUS_PATTERNS$1, listContractGadgets, HOOK_NAME_RE, STDLIB_GADGETS_PACKAGE, resolveAppGadgets } from '@ggui-ai/protocol';
3
+ import { classifyUi, MODEL_REGISTRY, DANGEROUS_PATTERNS as DANGEROUS_PATTERNS$1, listContractGadgets, HOOK_NAME_RE, STDLIB_GADGETS_PACKAGE, resolveAppGadgets } from '@ggui-ai/protocol';
4
4
  import * as esbuild from 'esbuild';
5
5
  import { build } from 'esbuild';
6
6
  import fs, { mkdirSync, writeFileSync, unlinkSync, readFileSync, existsSync, rm, createReadStream, createWriteStream } from 'fs';
@@ -30,11 +30,20 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
30
30
  if (typeof require !== "undefined") return require.apply(this, arguments);
31
31
  throw Error('Dynamic require of "' + x + '" is not supported');
32
32
  });
33
- var __esm = (fn, res) => function __init() {
34
- return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
33
+ var __esm = (fn, res, err) => function __init() {
34
+ if (err) throw err[0];
35
+ try {
36
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
37
+ } catch (e) {
38
+ throw err = [e], e;
39
+ }
35
40
  };
36
41
  var __commonJS = (cb, mod) => function __require2() {
37
- return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
42
+ try {
43
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
44
+ } catch (e) {
45
+ throw mod = 0, e;
46
+ }
38
47
  };
39
48
  var __export = (target, all) => {
40
49
  for (var name in all)
@@ -546,6 +555,8 @@ var init_types = __esm({
546
555
  this.code = code;
547
556
  this.name = "OpenRouterError";
548
557
  }
558
+ status;
559
+ code;
549
560
  get isTransient() {
550
561
  return this.status === 408 || this.status === 429 || this.status === 502 || this.status === 503;
551
562
  }
@@ -574,6 +585,7 @@ var init_client2 = __esm({
574
585
  "X-Title": config.siteName ?? process.env.OPENROUTER_SITE_NAME ?? "ggui"
575
586
  };
576
587
  }
588
+ config;
577
589
  baseUrl;
578
590
  headers;
579
591
  /**
@@ -677,7 +689,7 @@ function errorSummary(e) {
677
689
  }
678
690
  function interactionText(interaction) {
679
691
  let text = "";
680
- for (const step of interaction.steps) {
692
+ for (const step of interaction.steps ?? []) {
681
693
  if (step.type === "model_output") {
682
694
  for (const content of step.content ?? []) {
683
695
  if (content.type === "text") {
@@ -689,7 +701,7 @@ function interactionText(interaction) {
689
701
  return text;
690
702
  }
691
703
  function functionCallSteps(interaction) {
692
- return interaction.steps.filter(
704
+ return (interaction.steps ?? []).filter(
693
705
  (s) => s.type === "function_call"
694
706
  );
695
707
  }
@@ -1790,6 +1802,8 @@ var init_primitives = __esm({
1790
1802
  "EmptyState",
1791
1803
  "Stat",
1792
1804
  "Stepper",
1805
+ "Markdown",
1806
+ "MarkdownInline",
1793
1807
  "Header",
1794
1808
  "Sidebar",
1795
1809
  "CardGrid",
@@ -2236,6 +2250,7 @@ var init_ui_compiler = __esm({
2236
2250
  this.validation = validation;
2237
2251
  this.name = "UiValidationError";
2238
2252
  }
2253
+ validation;
2239
2254
  };
2240
2255
  UiBundleSizeError = class extends Error {
2241
2256
  constructor(size, limit) {
@@ -2244,6 +2259,8 @@ var init_ui_compiler = __esm({
2244
2259
  this.limit = limit;
2245
2260
  this.name = "UiBundleSizeError";
2246
2261
  }
2262
+ size;
2263
+ limit;
2247
2264
  };
2248
2265
  }
2249
2266
  });
@@ -4755,28 +4772,35 @@ var init_llm_evaluator = __esm({
4755
4772
  // src/evaluation/cost-tracker.ts
4756
4773
  var cost_tracker_exports = {};
4757
4774
  __export(cost_tracker_exports, {
4758
- CostTracker: () => CostTracker
4775
+ CostTracker: () => CostTracker,
4776
+ pricePer1k: () => pricePer1k
4759
4777
  });
4760
- var PRICE_PER_1K_TOKENS, CostTracker;
4778
+ function resolveRegistryId(model) {
4779
+ const keys = Object.keys(MODEL_REGISTRY);
4780
+ if (keys.includes(model)) return model;
4781
+ const bySuffix = keys.find((id) => id.endsWith(`/${model}`));
4782
+ if (bySuffix) return bySuffix;
4783
+ const undated = model.replace(/-\d{8}$/, "");
4784
+ return keys.find((id) => id.endsWith(`/${undated}`)) ?? null;
4785
+ }
4786
+ function pricePer1k(model) {
4787
+ const id = resolveRegistryId(model);
4788
+ if (id === null) return FALLBACK_PER_1K;
4789
+ const { costs } = MODEL_REGISTRY[id];
4790
+ return { input: costs.inputPer1M / 1e3, output: costs.outputPer1M / 1e3 };
4791
+ }
4792
+ var FALLBACK_PER_1K, CostTracker;
4761
4793
  var init_cost_tracker = __esm({
4762
4794
  "src/evaluation/cost-tracker.ts"() {
4763
- PRICE_PER_1K_TOKENS = {
4764
- "claude-sonnet-4-6": { input: 3e-3, output: 0.015 },
4765
- "claude-haiku-4-5-20251001": { input: 8e-4, output: 4e-3 },
4766
- "gemini-3-flash-preview": { input: 1e-4, output: 4e-4 },
4767
- // Gemini 3.5 Flash-Lite — $0.30/$2.50 per MTok (ai.google.dev/gemini-api/docs/pricing).
4768
- "gemini-3.5-flash-lite": { input: 3e-4, output: 25e-4 },
4769
- "gpt-5.4-mini": { input: 3e-4, output: 12e-4 },
4770
- // $1.00 in / $6.00 out per MTok (OpenAI list, 2026-07) → per-1K.
4771
- "gpt-5.6-luna": { input: 1e-3, output: 6e-3 }
4772
- };
4795
+ FALLBACK_PER_1K = { input: 3e-3, output: 0.015 };
4773
4796
  CostTracker = class {
4774
4797
  constructor(maxBudget) {
4775
4798
  this.maxBudget = maxBudget;
4776
4799
  }
4800
+ maxBudget;
4777
4801
  totalCost = 0;
4778
4802
  record(model, inputTokens, outputTokens) {
4779
- const prices = PRICE_PER_1K_TOKENS[model] ?? { input: 3e-3, output: 0.015 };
4803
+ const prices = pricePer1k(model);
4780
4804
  const cost = inputTokens / 1e3 * prices.input + outputTokens / 1e3 * prices.output;
4781
4805
  this.totalCost += cost;
4782
4806
  return cost;
@@ -10071,7 +10095,7 @@ function inspect(contract) {
10071
10095
  );
10072
10096
  actions.push({
10073
10097
  name,
10074
- tool: a?.tool,
10098
+ nextStep: a?.nextStep,
10075
10099
  example: a?.example,
10076
10100
  scalarKeys,
10077
10101
  allKeys,
@@ -10318,14 +10342,10 @@ function inferLayout(s, prompt, blueprint) {
10318
10342
  var DRAG_RX = /\b(drag|drop|dragging|draggable)\b/i;
10319
10343
  var SWIPE_RX = /\b(swipe|swipes|gesture\s*stack)\b/i;
10320
10344
  var KEYBOARD_RX = /\b(keyboard\s*shortcut|hotkey|keystroke|kbd)\b/i;
10321
- function inferWriteTrigger(s, prompt, blueprint) {
10345
+ function inferWriteTrigger(s, prompt) {
10322
10346
  if (s.actions.length === 0) {
10323
10347
  return { value: "click", source: "default" };
10324
10348
  }
10325
- if (blueprint?.mechanic === "drag")
10326
- return { value: "drag", source: "blueprint" };
10327
- if (blueprint?.mechanic === "swipe")
10328
- return { value: "swipe", source: "blueprint" };
10329
10349
  if (DRAG_RX.test(prompt)) return { value: "drag", source: "prompt" };
10330
10350
  if (SWIPE_RX.test(prompt)) return { value: "swipe", source: "prompt" };
10331
10351
  if (KEYBOARD_RX.test(prompt)) return { value: "keystroke", source: "prompt" };
@@ -10366,7 +10386,7 @@ function classifyAxes(input) {
10366
10386
  const fetch2 = inferFetch(s);
10367
10387
  const render = inferRender(s, prompt, blueprint);
10368
10388
  const layout = inferLayout(s, prompt, blueprint);
10369
- const writeTrigger = inferWriteTrigger(s, prompt, blueprint);
10389
+ const writeTrigger = inferWriteTrigger(s, prompt);
10370
10390
  const tooling = inferTooling(s);
10371
10391
  const vector = {
10372
10392
  render: render.value,
@@ -11188,28 +11208,17 @@ function inferTypeFromExample(example) {
11188
11208
  }
11189
11209
  function generateBoilerplate(_userPrompt, contract, shellType, screen, composedSections, appGadgets) {
11190
11210
  const propsFields = [];
11191
- const propsData = contract?.propsSpec;
11192
- const propsProperties = propsData?.properties ?? propsData ?? {};
11193
- for (const [key, value] of Object.entries(propsProperties)) {
11194
- if (typeof value === "object" && value !== null) {
11195
- const spec = value;
11196
- const schema = spec.schema;
11197
- const required = spec.required !== false;
11198
- const nullable = schema?.nullable === true;
11199
- const tsType = schema ? (
11200
- // JsonSchema extends JsonObject, so this is a plain structural
11201
- // downcast (no `unknown` erasure) of already-shaped contract data.
11202
- jsonSchemaTypeToTs(schema)
11203
- ) : "unknown";
11204
- const fullType = nullable ? `${tsType} | null` : tsType;
11205
- const parts = [];
11206
- if (spec.description) parts.push(String(spec.description));
11207
- if (spec.default !== void 0) parts.push(`(default: ${JSON.stringify(spec.default)})`);
11208
- const desc = parts.length > 0 ? ` // ${parts.join(" ")}` : "";
11209
- propsFields.push(` ${key}${required ? "" : "?"}: ${fullType};${desc}`);
11210
- } else {
11211
- propsFields.push(` ${key}: ${typeof value === "string" ? value : "unknown"};`);
11212
- }
11211
+ const propsProperties = contract?.propsSpec?.properties ?? {};
11212
+ for (const [key, entry] of Object.entries(propsProperties)) {
11213
+ const required = entry.required !== false;
11214
+ const nullable = entry.schema.nullable === true;
11215
+ const tsType = jsonSchemaTypeToTs(entry.schema);
11216
+ const fullType = nullable ? `${tsType} | null` : tsType;
11217
+ const parts = [];
11218
+ if (entry.description) parts.push(entry.description);
11219
+ if (entry.default !== void 0) parts.push(`(default: ${JSON.stringify(entry.default)})`);
11220
+ const desc = parts.length > 0 ? ` // ${parts.join(" ")}` : "";
11221
+ propsFields.push(` ${key}${required ? "" : "?"}: ${fullType};${desc}`);
11213
11222
  }
11214
11223
  const actionTypeAliases = [];
11215
11224
  const actionHookCalls = [];
@@ -11766,10 +11775,10 @@ A \`DataContract\` declares everything a GguiSession exchanges with the outside
11766
11775
 
11767
11776
  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.**
11768
11777
 
11769
- - **Array iteration**: always default to \`[]\` before \`.map\`/\`.filter\`/\`.length\`. Use \`(props.items ?? []).map(...)\` not \`props.items.map(...)\`. Same for stream.history, stream.latest, etc.
11778
+ - **Array iteration**: always default to \`[]\` before \`.map\`/\`.filter\`/\`.length\`. Use \`(props.items ?? []).map(...)\` not \`props.items.map(...)\`. Same for stream.all, stream.latest, etc.
11770
11779
  - **Object access**: optional-chain through nested fields. \`props.user?.name ?? 'Anonymous'\` not \`props.user.name\`.
11771
11780
  - **Number ops**: default before arithmetic. \`(props.count ?? 0) + 1\` not \`props.count + 1\`.
11772
- - **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\`.
11781
+ - **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\`.
11773
11782
  - **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.
11774
11783
  - **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.
11775
11784
  - **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.
@@ -12990,7 +12999,7 @@ tighter on display sizes).
12990
12999
  init_validation();
12991
13000
 
12992
13001
  // src/tools/get-wire.ts
12993
- 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";
13002
+ 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";
12994
13003
 
12995
13004
  // src/harness/pitfalls.ts
12996
13005
  var PITFALLS = [