@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.
- package/dist/adapters/base.js +1 -0
- package/dist/adapters/base.js.map +1 -1
- package/dist/adapters/generation-dispatch.js +60 -51
- package/dist/adapters/generation-dispatch.js.map +1 -1
- package/dist/adapters/index.js +13 -2
- package/dist/adapters/index.js.map +1 -1
- package/dist/advanced/index.js +61 -51
- package/dist/advanced/index.js.map +1 -1
- package/dist/blueprint-validator.js +1 -1
- package/dist/blueprint-validator.js.map +1 -1
- package/dist/boilerplate.js +13 -24
- package/dist/boilerplate.js.map +1 -1
- package/dist/check/index.d.ts +2 -2
- package/dist/check/index.js.map +1 -1
- package/dist/classifier/index.d.ts +14 -17
- package/dist/classifier/index.js +3 -7
- package/dist/classifier/index.js.map +1 -1
- package/dist/evaluation/index.js +12 -4
- package/dist/evaluation/index.js.map +1 -1
- package/dist/harness/check/runtime-render/index.js.map +1 -1
- package/dist/harness/index.js +25 -28
- package/dist/harness/index.js.map +1 -1
- package/dist/harness/runtime.js +14 -25
- package/dist/harness/runtime.js.map +1 -1
- package/dist/harness/types-public.js +13 -24
- package/dist/harness/types-public.js.map +1 -1
- package/dist/index.js +62 -51
- package/dist/index.js.map +1 -1
- package/dist/provider-adapter-contract.js +12 -3
- package/dist/provider-adapter-contract.js.map +1 -1
- package/dist/providers/index.d.ts +2 -2
- package/dist/providers/index.js.map +1 -1
- package/dist/validation/index.d.ts +1 -1
- package/dist/validation/index.js +5 -0
- package/dist/validation/index.js.map +1 -1
- package/package.json +12 -12
- package/src/blueprint-validator.ts +1 -1
- package/src/boilerplate/generate.ts +20 -31
- package/src/boilerplate/system-prompt.ts +2 -2
- package/src/check/extract-wire-calls.ts +2 -2
- package/src/classifier/axes.ts +2 -2
- package/src/classifier/classifier.ts +2 -2
- package/src/classifier/infer-layout.ts +1 -1
- package/src/classifier/infer-render.ts +1 -1
- package/src/classifier/infer-tooling.ts +1 -1
- package/src/classifier/infer-trigger.ts +3 -12
- package/src/classifier/inspect.ts +7 -7
- package/src/coding-agent/self-check.ts +1 -1
- package/src/evaluation/cost-tracker.ts +39 -11
- package/src/harness/check/runtime-render/render-check.ts +4 -4
- package/src/harness/llm-router.ts +2 -2
- package/src/providers/bedrock.ts +2 -2
- package/src/tools/get-wire.ts +1 -1
- package/src/validation/primitives.ts +2 -0
package/dist/adapters/base.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
11192
|
-
const
|
|
11193
|
-
|
|
11194
|
-
|
|
11195
|
-
|
|
11196
|
-
|
|
11197
|
-
|
|
11198
|
-
|
|
11199
|
-
|
|
11200
|
-
|
|
11201
|
-
|
|
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.
|
|
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 |
|
|
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
|
|
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 = [
|