@ggui-ai/ui-gen 0.18.0 → 0.20.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/generation-dispatch.d.ts +1 -1
- package/dist/adapters/generation-dispatch.js +248 -88
- package/dist/adapters/generation-dispatch.js.map +1 -1
- package/dist/adapters/index.js +39 -16
- package/dist/adapters/index.js.map +1 -1
- package/dist/adapters/types.d.ts +1 -1
- package/dist/adapters/types.js.map +1 -1
- package/dist/advanced/index.d.ts +2 -2
- package/dist/advanced/index.js +217 -34
- package/dist/advanced/index.js.map +1 -1
- package/dist/blueprint-validator.js +5 -2
- package/dist/blueprint-validator.js.map +1 -1
- package/dist/boilerplate.js +2 -2
- package/dist/boilerplate.js.map +1 -1
- package/dist/check/index.d.ts +1 -1
- package/dist/check/index.js.map +1 -1
- package/dist/{create-ui-generator-C8Ki9Wz1.d.ts → create-ui-generator-CxPvudbs.d.ts} +1 -1
- package/dist/{dispatch-D2htzi9h.d.ts → dispatch-N8wdNOX-.d.ts} +4 -2
- package/dist/evaluation/axis-checks/index.d.ts +3 -3
- package/dist/evaluation/axis-checks/index.js +143 -3
- package/dist/evaluation/axis-checks/index.js.map +1 -1
- package/dist/evaluation/axis-checks/registry.d.ts +1 -1
- package/dist/evaluation/axis-checks/registry.js +138 -1
- package/dist/evaluation/axis-checks/registry.js.map +1 -1
- package/dist/evaluation/index.d.ts +3 -3
- package/dist/evaluation/index.js +213 -84
- package/dist/evaluation/index.js.map +1 -1
- package/dist/harness/check/runtime-render/index.d.ts +1 -1
- package/dist/harness/check/runtime-render/index.js +5 -2
- package/dist/harness/check/runtime-render/index.js.map +1 -1
- package/dist/harness/check/runtime-render/render-check-worker.js +5 -2
- package/dist/harness/check/runtime-render/render-check-worker.js.map +1 -1
- package/dist/harness/index.d.ts +1 -1
- package/dist/harness/index.js +64 -61
- package/dist/harness/index.js.map +1 -1
- package/dist/harness/prompts.js +1 -1
- package/dist/harness/prompts.js.map +1 -1
- package/dist/harness/result-types.d.ts +1 -1
- package/dist/harness/runtime.d.ts +1 -1
- package/dist/harness/runtime.js +4 -4
- package/dist/harness/runtime.js.map +1 -1
- package/dist/harness/types-public.d.ts +8 -1
- package/dist/harness/types-public.js +64 -61
- package/dist/harness/types-public.js.map +1 -1
- package/dist/harness/validator-trace-sink.d.ts +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +217 -34
- package/dist/index.js.map +1 -1
- package/dist/patch.d.ts +1 -1
- package/dist/{types-public-DVSssdc-.d.ts → types-public-DKR_h70H.d.ts} +9 -0
- package/dist/validation/index.d.ts +1 -1
- package/dist/validation/index.js +1 -1
- package/dist/validation/index.js.map +1 -1
- package/dist/workflows.d.ts +1 -1
- package/package.json +12 -12
- package/src/adapters/google/sdk.ts +11 -7
- package/src/adapters/openai/raw.ts +11 -5
- package/src/adapters/openai/sdk.ts +8 -5
- package/src/adapters/openai/tokens.ts +49 -0
- package/src/adapters/types.ts +1 -1
- package/src/boilerplate/hard-sections.ts +1 -1
- package/src/boilerplate/system-prompt.ts +1 -1
- package/src/coding-agent/tools.ts +58 -6
- package/src/contract-context.ts +1 -1
- package/src/create-harness.ts +1 -0
- package/src/evaluation/axis-checks/checks/universal.ts +207 -1
- package/src/evaluation/axis-checks/dispatch.ts +8 -1
- package/src/evaluation/types-public.ts +9 -0
- package/src/harness/check/runtime-render/render-check.ts +15 -0
- package/src/harness/coding/generate-task-runner.ts +1 -0
- package/src/harness/coding/run-coding-turn.ts +14 -0
- package/src/harness/types-public.ts +7 -0
- package/src/run-check.ts +2 -0
- package/src/tools/get-wire.ts +1 -1
- package/src/validation/primitives.ts +1 -1
|
@@ -9,7 +9,7 @@ import { REGISTRY } from "./registry.js";
|
|
|
9
9
|
import { matches, type AxisCheck, type AxisCheckInput } from "./types.js";
|
|
10
10
|
import { axisCheckTraceEnabled } from "./helpers.js";
|
|
11
11
|
import { createHash } from "node:crypto";
|
|
12
|
-
import type { DesignMode } from "../../design-mode.js";
|
|
12
|
+
import type { CanvasClass, DesignMode } from "../../design-mode.js";
|
|
13
13
|
|
|
14
14
|
export interface RunAxisChecksInput {
|
|
15
15
|
sourceCode: string;
|
|
@@ -18,6 +18,8 @@ export interface RunAxisChecksInput {
|
|
|
18
18
|
originalPrompt: string;
|
|
19
19
|
/** Which triad produced the source — see `AxisCheckInput.designMode`. */
|
|
20
20
|
designMode?: DesignMode;
|
|
21
|
+
/** The canvas the source is judged for — see `AxisCheckInput.canvas` (ggui#1117); absent = no rendering context. */
|
|
22
|
+
canvas?: CanvasClass;
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
export function runAxisChecks(
|
|
@@ -30,6 +32,7 @@ export function runAxisChecks(
|
|
|
30
32
|
originalPrompt: input.originalPrompt,
|
|
31
33
|
classification,
|
|
32
34
|
...(input.designMode !== undefined ? { designMode: input.designMode } : {}),
|
|
35
|
+
...(input.canvas !== undefined ? { canvas: input.canvas } : {}),
|
|
33
36
|
};
|
|
34
37
|
if (input.compiledCode === null) {
|
|
35
38
|
traceAxisChecksSkipped(facts, "compiledCode null — no check ran");
|
|
@@ -93,6 +96,8 @@ export interface AxisCheckTrace {
|
|
|
93
96
|
readonly classification: Classification["vector"];
|
|
94
97
|
readonly propsSpecKeys: readonly string[];
|
|
95
98
|
readonly propsSpecPropertyKeys: readonly string[];
|
|
99
|
+
/** The canvas the verdict was built for (ggui#1117) — present only when the input carried one. */
|
|
100
|
+
readonly canvas?: CanvasClass;
|
|
96
101
|
readonly matched: readonly string[];
|
|
97
102
|
readonly issues: ReadonlyArray<{ readonly id: string; readonly result: EvalIssue["result"] }>;
|
|
98
103
|
readonly note?: string;
|
|
@@ -107,5 +112,7 @@ function traceFacts(input: AxisTraceFacts): Omit<AxisCheckTrace, "matched" | "is
|
|
|
107
112
|
classification: input.classification.vector,
|
|
108
113
|
propsSpecKeys: Object.keys(propsSpec ?? {}),
|
|
109
114
|
propsSpecPropertyKeys: Object.keys(propsSpec?.properties ?? {}),
|
|
115
|
+
// No default here, unlike designMode: an absent canvas is a fact about the caller, not a guess to paper over.
|
|
116
|
+
...(input.canvas !== undefined ? { canvas: input.canvas } : {}),
|
|
110
117
|
};
|
|
111
118
|
}
|
|
@@ -280,6 +280,15 @@ export interface AxisCheckInput {
|
|
|
280
280
|
* means `constrained`.
|
|
281
281
|
*/
|
|
282
282
|
designMode?: DesignMode;
|
|
283
|
+
/**
|
|
284
|
+
* The rendering canvas the source is judged FOR (ggui#1117). A width cap on
|
|
285
|
+
* the outermost element is a defect on a fullscreen canvas and a no-op in a
|
|
286
|
+
* chat bubble, so a check that reads width must know the surface. Populated
|
|
287
|
+
* where the harness is built (`Harness.canvas`); ABSENT for callers with no
|
|
288
|
+
* rendering context — a check handles `undefined`, it never assumes a
|
|
289
|
+
* default (a silent `lg` would be the same guess with a nicer face).
|
|
290
|
+
*/
|
|
291
|
+
canvas?: CanvasClass;
|
|
283
292
|
}
|
|
284
293
|
|
|
285
294
|
/**
|
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
type ParentLink,
|
|
46
46
|
} from "./host-boundary.js";
|
|
47
47
|
import { hostGlobals, openRecord } from "../../../internal/open-record.js";
|
|
48
|
+
import { primeInputs } from "@ggui-ai/ui-visual-tester/prime-inputs";
|
|
48
49
|
|
|
49
50
|
/**
|
|
50
51
|
* Module-namespace-shaped value injected into `loadComponent`'s
|
|
@@ -504,6 +505,20 @@ export async function runRenderCheckInProcess(
|
|
|
504
505
|
let streamsChecked = 0;
|
|
505
506
|
|
|
506
507
|
try {
|
|
508
|
+
// ggui#1187: prime form inputs before the action-wiring probe. A wired
|
|
509
|
+
// Send/Submit gated on a non-empty input (the chat-interface shape) is a
|
|
510
|
+
// no-op on empty, so an un-primed synthetic click never dispatches and
|
|
511
|
+
// the probe false-warns "did not dispatch it". Shared with the visual
|
|
512
|
+
// tester's primeInputs (#1021/#1040) so both prime identically. Priming
|
|
513
|
+
// sets input VALUES (not textContent) and fires input/change before the
|
|
514
|
+
// per-candidate fire-log window is opened, so it neither perturbs the
|
|
515
|
+
// prop-sensitivity text baseline nor false-positives the wiring probe.
|
|
516
|
+
// Best-effort: a priming failure must never abort the check.
|
|
517
|
+
try {
|
|
518
|
+
primeInputs(renderResult.container);
|
|
519
|
+
} catch {
|
|
520
|
+
// Un-primeable DOM falls back to the prior (un-primed) behavior.
|
|
521
|
+
}
|
|
507
522
|
// ── Check 2: Action wiring (BLOCK / unverified-as-warn) ───────────
|
|
508
523
|
if (input.contract?.actionSpec) {
|
|
509
524
|
const actionSpec = input.contract.actionSpec;
|
|
@@ -178,6 +178,7 @@ export function createGenerateTaskRunner(input: CreateGenerateRunnerInput): Task
|
|
|
178
178
|
systemPrompt: session.systemPrompt,
|
|
179
179
|
harness: session.harness,
|
|
180
180
|
contract: params.contract,
|
|
181
|
+
originalPrompt: params.userPrompt,
|
|
181
182
|
commitMeta: session.commitMeta,
|
|
182
183
|
originalProps: session.originalProps,
|
|
183
184
|
costTracker: session.costTracker,
|
|
@@ -155,6 +155,13 @@ export interface CodingTurnContext {
|
|
|
155
155
|
readonly systemPrompt: string;
|
|
156
156
|
readonly harness: Harness;
|
|
157
157
|
readonly contract: DataContract | undefined;
|
|
158
|
+
/**
|
|
159
|
+
* The ORIGINAL request (ggui#1122) — what the eval round has always judged
|
|
160
|
+
* against, now handed to auto-commit so the axis checks run on the serve
|
|
161
|
+
* path with the same input. Required: a caller that forgot it would silently
|
|
162
|
+
* run no axis checks on every served generation.
|
|
163
|
+
*/
|
|
164
|
+
readonly originalPrompt: string;
|
|
158
165
|
readonly commitMeta: Map<string, CommitMetadata>;
|
|
159
166
|
readonly originalProps: string | undefined;
|
|
160
167
|
readonly costTracker: CostTracker | null;
|
|
@@ -669,6 +676,13 @@ ${closingInstruction}`;
|
|
|
669
676
|
// Which tier-0 legs fire — the design-vocabulary legs stand down in
|
|
670
677
|
// `free` mode; every contract leg runs in both.
|
|
671
678
|
harness.designMode,
|
|
679
|
+
// ggui#1122: the harness's pre-filtered axis checks run at auto-commit — the serve path.
|
|
680
|
+
{
|
|
681
|
+
checks: harness.check.axisChecks,
|
|
682
|
+
classification: harness.classification,
|
|
683
|
+
originalPrompt: ctx.originalPrompt,
|
|
684
|
+
...(harness.canvas !== undefined ? { canvas: harness.canvas } : {}),
|
|
685
|
+
},
|
|
672
686
|
);
|
|
673
687
|
const toolMs = Date.now() - toolStart;
|
|
674
688
|
|
|
@@ -303,6 +303,13 @@ export interface Harness {
|
|
|
303
303
|
* into `free`.
|
|
304
304
|
*/
|
|
305
305
|
readonly designMode: DesignMode;
|
|
306
|
+
/**
|
|
307
|
+
* The rendering canvas this harness was built for (ggui#1117) — threaded
|
|
308
|
+
* from `CreateHarnessInput.canvas` into the HOW leg (the `free` prompt
|
|
309
|
+
* states it) and, via `run-check.ts`, onto every axis check's input.
|
|
310
|
+
* ABSENT when the caller had no rendering context; never defaulted.
|
|
311
|
+
*/
|
|
312
|
+
readonly canvas?: CanvasClass;
|
|
306
313
|
/** The app's generation profile in force for this harness (#991), if any. */
|
|
307
314
|
readonly profile?: GenerationProfileInput;
|
|
308
315
|
|
package/src/run-check.ts
CHANGED
|
@@ -119,6 +119,8 @@ export async function runCheck(input: RunCheckInput): Promise<CheckResult> {
|
|
|
119
119
|
originalPrompt: prompt,
|
|
120
120
|
classification: harness.classification,
|
|
121
121
|
designMode: harness.designMode,
|
|
122
|
+
// ggui#1117: the canvas the harness was built for, when it has one — never a default.
|
|
123
|
+
...(harness.canvas !== undefined ? { canvas: harness.canvas } : {}),
|
|
122
124
|
};
|
|
123
125
|
// The one runner `runAxisChecks` shares (ggui#1046 trace prints here, on the served path).
|
|
124
126
|
const axisRun = runGatedAxisChecks(check.axisChecks, axisInput);
|
package/src/tools/get-wire.ts
CHANGED
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
* AUTO-GENERATED from @ggui-ai/wire JSDoc.
|
|
3
3
|
* Do not edit manually. Run: pnpm --filter @ggui-ai/wire generate:docs
|
|
4
4
|
*/
|
|
5
|
-
export const WIRE_DOCUMENTATION = "# ggui Wire Hooks Reference\n\n> Wire hooks connect generated UI components to agent communication.\n> They are pre-imported in the boilerplate — use them directly.\n> All hooks must be called inside a GguiWireProvider (handled automatically by the renderer).\n\nImport: `import { useAction, useStream } from '@ggui-ai/wire'`\n\n## Communication Hooks\n\n\n\nThese are the wire primitives for component-agent communication. `useWiredTool` retired 2026-05-11 — agentCapabilities.tools is a catalog the AGENT invokes, not a component hook surface; user gestures use `useAction(name)` and the optional `nextStep` field on the action entry names the tool the agent SHOULD invoke next.\n\n### useAction\n\nFire an action to the agent. Fire-and-forget — no response, no pending state.\n\nEvery action is an event: it lands on the GguiSession's consume buffer and\nthe agent receives it on its next turn (via `ggui_consume`). The action\nentry's optional `nextStep` names the tool the agent SHOULD invoke in\nresponse — advisory only; the agent owns the call decision. Component code\nis identical either way: call `useAction(name)(payload)` and treat\n`nextStep` as informational (use it to inform button labels, icons, copy).\n\nRUNTIME DEDUP (backstop, not a feature). Same-`(name, payload)` calls within\none event-loop task are coalesced — the first wins, subsequent duplicates\nare suppressed. This is the structural defense against LLM-generated\nnested-interactive components where a Checkbox `onChange` and an outer\n`Card as={Clickable}` `onClick` both wire to the same `useAction` binding;\nthe inner gesture bubbles to the outer handler, so without dedup one user\nclick would fire the action twice (a toggle would run back-to-back and the\nuser's change would disappear). **Do not rely on this as a feature** — wire\neach `useAction` callback to exactly ONE interactive surface; the dedup\nexists only because the LLM's source code can be wrong in subtle ways and\nthe runtime is the only place that can see the actual event-bubble path.\nSee `dispatch-dedup.ts` for the full failure-mode rationale.\n\nNEVER SILENT. When the dedup fires, a `console.warn` is emitted in BOTH dev\nand prod with the full diagnostic. The suppression is always visible in\nbrowser DevTools; operators investigating a \"the second click does nothing\"\nreport see the warning immediately. A structured observer also exists as\n`WireConfig.onDispatchSuppressed`, but the first-party renderers never set\nit — it is reachable only from a hand-built `WireConfig` (see its\ndocstring), so the console signal is what operators get in practice.\n\n**Signature:** `useAction<T = unknown>(actionName: string): (data: T) => void`\n\n**Parameters:**\n\n| Param | Type | Description |\n|-------|------|-------------|\n| actionName | `string` | Action name from the action contract |\n\n**Returns:** `(data: T) => void`\n\n**Example:**\n```tsx\nconst submitForm = useAction<{name: string; email: string}>('formSubmit');\n\n// In JSX:\n<Button onClick={() => submitForm({ name, email })}>Submit</Button>\n```\n\n\n### useStream\n\nSubscribe to deliveries on a named stream channel.\n\nHonors the channel's per-delivery `mode` ('append' vs 'replace')\nand the optional `complete` terminal marker. Channels declared\n`mode: 'replace'` on the spec typically emit every delivery with\n`mode: 'replace'` — this hook folds them into a single-latest\nvalue without accumulating history.\n\n**Signature:** `useStream<T = unknown>(channelName: string): StreamResult<T>`\n\n**Parameters:**\n\n| Param | Type | Description |\n|-------|------|-------------|\n| channelName | `string` | Channel name from the GguiSession's streamSpec |\n\n**Returns:** `StreamResult<T>`\n\n| Property | Type | Description |\n|----------|------|-------------|\n| latest | `T \\| null` | Most recent payload delivered on this channel, or null if none received yet. |\n| all | `T[]` | All payloads accumulated on this channel. - `mode: 'append'` deliveries are pushed to the tail (continuous stream). - `mode: 'replace'` deliveries collapse `all` to a single-element array containing the latest payload — matching the channel's \"full replacement\" semantics. |\n| isComplete | `boolean` | Truthy after the channel has delivered an envelope with `complete: true`. Subscribers flip into a \"channel closed\" rendering state based on this signal; further deliveries on a completed channel are still accumulated, since the underlying wire doesn't enforce quiescence. |\n\n**Example:**\n```tsx\nconst progress = useStream<{ percent: number; message: string }>('progress');\n\n// In JSX:\n{progress.latest && (\n <Progress value={progress.latest.percent} />\n)}\n<Text>{progress.all.length} updates received</Text>\n```\n\n\n## Context Hooks\n\n\n\nRead-only access to render, app, and auth context.\n\n### useAuth\n\nRead-only auth context. Token excluded — auth is added server-side.\n\n**Signature:** `useAuth(): AuthInfo`\n\n**Returns:** `AuthInfo`\n\n| Property | Type | Description |\n|----------|------|-------------|\n| userId | `string` | userId |\n| isAuthenticated | `boolean` | isAuthenticated |\n\n**Example:**\n```tsx\nconst auth = useAuth();\n\n// In JSX:\n{auth.isAuthenticated\n ? <Text>Welcome, user {auth.userId}</Text>\n : <Text>Please sign in</Text>\n}\n```\n\n\n### useApp\n\nRead-only app metadata.\n\n**Signature:** `useApp(): AppInfo`\n\n**Returns:** `AppInfo`\n\n| Property | Type | Description |\n|----------|------|-------------|\n| appId | `string` | appId |\n| appName | `string` | appName |\n| appDescription | `string` | appDescription |\n| appIcon | `string` | appIcon |\n\n**Example:**\n```tsx\nconst app = useApp();\n\n// In JSX:\n<Heading>{app.appName}</Heading>\n{app.appDescription && <Text>{app.appDescription}</Text>}\n```\n\n\n### useRender\n\nRead-only render context with connection status.\n\n**Signature:** `useRender(): GguiSessionInfo`\n\n**Returns:** `GguiSessionInfo`\n\n| Property | Type | Description |\n|----------|------|-------------|\n| sessionId | `string` | sessionId |\n| isConnected | `boolean` | isConnected |\n\n**Example:**\n```tsx\nconst render = useRender();\n\n// In JSX:\n<Badge variant={render.isConnected ? 'success' : 'error'}>\n {render.isConnected ? 'Connected' : 'Disconnected'}\n</Badge>\n```\n\n\n## Provider\n\n\n\nThe provider is set up automatically by the renderer. Generated components do not need to wrap themselves in a provider.\n\n### GguiWireProvider\n\nReact context provider that injects WireConfig for all wire hooks.\n\n**Props:**\n\n| Prop | Type | Description |\n|------|------|-------------|\n| config | `WireConfig` | config |\n| children | `ReactNode` | children |\n\n**Example:**\n```tsx\n<GguiWireProvider config={wireConfig}>\n <YourComponent />\n</GguiWireProvider>\n```\n\n\n## Internal: WireConfig\n\n\n\nThis is the configuration object passed to GguiWireProvider. Generated components do not interact with this directly — it is provided by the renderer.\n\n### WireConfig\n\nConfiguration injected by the provider — the renderer inside the\niframe.\n\nEvery method is typed against the contract generic `T` so typed\ncallers get compile-time enforcement:\n - `dispatch(name, data)` — `name` MUST be a declared actionSpec\n key; `data` MUST satisfy that action's schema.\n - `subscribe(channel, handler)` — same discipline for streamSpec.\n\nUntyped callers (`T = DataContract` default) degrade to the broad\nshape via the conditional `WireDispatchData` / `WireStreamPayload`\naliases — no call-site break.\n\nThe contract's `agentCapabilities.tools` catalog declares tools the AGENT\ninvokes (not the component); user gestures fire via\n`dispatch(name, data)` and the optional `nextStep` field on the\naction entry names the tool the agent SHOULD invoke next.\n\nThe renderer mounts exactly ONE GguiSession per iframe — `render.sessionId`\nis the stable identity the WireProvider was constructed with. No\nper-item scoping factory; with a one-GguiSession-per-mount lifecycle,\n\"scope\" collapses to identity.\n\n| Property | Type | Description |\n|----------|------|-------------|\n| app | `{ readonly appId: string; readonly appName: string; readonly appDescription?: string; readonly appIcon?: string; }` | app |\n| render | `{ readonly sessionId: string; readonly isConnected: boolean; }` | render |\n| auth | `{ readonly userId?: string; readonly isAuthenticated: boolean; }` | auth |\n| dispatch | `<N extends string>( actionName: N, data: WireDispatchData<T, N>, ) => void` | Fire an action to the agent (fire-and-forget over WS). Typed callers get compile-time checked `name` + `data`; untyped callers (`T = DataContract`) keep the broad shape. |\n| subscribe | `<N extends string>( channelName: N, handler: (delivery: StreamDelivery<WireStreamPayload<T, N>>) => void, ) => () => void` | Subscribe to deliveries on a named stream channel. |\n| onDispatchSuppressed | `(info: DispatchSuppressedInfo) => void` | Optional structured observability for
|
|
5
|
+
export const WIRE_DOCUMENTATION = "# ggui Wire Hooks Reference\n\n> Wire hooks connect generated UI components to agent communication.\n> They are pre-imported in the boilerplate — use them directly.\n> All hooks must be called inside a GguiWireProvider (handled automatically by the renderer).\n\nImport: `import { useAction, useStream } from '@ggui-ai/wire'`\n\n## Communication Hooks\n\n\n\nThese are the wire primitives for component-agent communication. `useWiredTool` retired 2026-05-11 — agentCapabilities.tools is a catalog the AGENT invokes, not a component hook surface; user gestures use `useAction(name)` and the optional `nextStep` field on the action entry names the tool the agent SHOULD invoke next.\n\n### useAction\n\nFire an action to the agent. Fire-and-forget — no response, no pending state.\n\nEvery action is an event: it lands on the GguiSession's consume buffer and\nthe agent receives it on its next turn (via `ggui_consume`). The action\nentry's optional `nextStep` names the tool the agent SHOULD invoke in\nresponse — advisory only; the agent owns the call decision. Component code\nis identical either way: call `useAction(name)(payload)` and treat\n`nextStep` as informational (use it to inform button labels, icons, copy).\n\nRUNTIME DEDUP (backstop, not a feature). Same-`(name, payload)` calls within\none event-loop task are coalesced — the first wins, subsequent duplicates\nare suppressed. This is the structural defense against LLM-generated\nnested-interactive components where a Checkbox `onChange` and an outer\n`Card as={Clickable}` `onClick` both wire to the same `useAction` binding;\nthe inner gesture bubbles to the outer handler, so without dedup one user\nclick would fire the action twice (a toggle would run back-to-back and the\nuser's change would disappear). **Do not rely on this as a feature** — wire\neach `useAction` callback to exactly ONE interactive surface; the dedup\nexists only because the LLM's source code can be wrong in subtle ways and\nthe runtime is the only place that can see the actual event-bubble path.\nSee `dispatch-dedup.ts` for the full failure-mode rationale.\n\nNEVER SILENT. When the dedup fires, a `console.warn` is emitted in BOTH dev\nand prod with the full diagnostic. The suppression is always visible in\nbrowser DevTools; operators investigating a \"the second click does nothing\"\nreport see the warning immediately. A structured observer also exists as\n`WireConfig.onDispatchSuppressed`, but the first-party renderers never set\nit — it is reachable only from a hand-built `WireConfig` (see its\ndocstring), so the console signal is what operators get in practice.\n\n**Signature:** `useAction<T = unknown>(actionName: string): (data: T) => void`\n\n**Parameters:**\n\n| Param | Type | Description |\n|-------|------|-------------|\n| actionName | `string` | Action name from the action contract |\n\n**Returns:** `(data: T) => void`\n\n**Example:**\n```tsx\nconst submitForm = useAction<{name: string; email: string}>('formSubmit');\n\n// In JSX:\n<Button onClick={() => submitForm({ name, email })}>Submit</Button>\n```\n\n\n### useStream\n\nSubscribe to deliveries on a named stream channel.\n\nHonors the channel's per-delivery `mode` ('append' vs 'replace')\nand the optional `complete` terminal marker. Channels declared\n`mode: 'replace'` on the spec typically emit every delivery with\n`mode: 'replace'` — this hook folds them into a single-latest\nvalue without accumulating history.\n\n**Signature:** `useStream<T = unknown>(channelName: string): StreamResult<T>`\n\n**Parameters:**\n\n| Param | Type | Description |\n|-------|------|-------------|\n| channelName | `string` | Channel name from the GguiSession's streamSpec |\n\n**Returns:** `StreamResult<T>`\n\n| Property | Type | Description |\n|----------|------|-------------|\n| latest | `T \\| null` | Most recent payload delivered on this channel, or null if none received yet. |\n| all | `T[]` | All payloads accumulated on this channel. - `mode: 'append'` deliveries are pushed to the tail (continuous stream). - `mode: 'replace'` deliveries collapse `all` to a single-element array containing the latest payload — matching the channel's \"full replacement\" semantics. |\n| isComplete | `boolean` | Truthy after the channel has delivered an envelope with `complete: true`. Subscribers flip into a \"channel closed\" rendering state based on this signal; further deliveries on a completed channel are still accumulated, since the underlying wire doesn't enforce quiescence. |\n\n**Example:**\n```tsx\nconst progress = useStream<{ percent: number; message: string }>('progress');\n\n// In JSX:\n{progress.latest && (\n <Progress value={progress.latest.percent} />\n)}\n<Text>{progress.all.length} updates received</Text>\n```\n\n\n## Context Hooks\n\n\n\nRead-only access to render, app, and auth context.\n\n### useAuth\n\nRead-only auth context. Token excluded — auth is added server-side.\n\n**Signature:** `useAuth(): AuthInfo`\n\n**Returns:** `AuthInfo`\n\n| Property | Type | Description |\n|----------|------|-------------|\n| userId | `string` | userId |\n| isAuthenticated | `boolean` | isAuthenticated |\n\n**Example:**\n```tsx\nconst auth = useAuth();\n\n// In JSX:\n{auth.isAuthenticated\n ? <Text>Welcome, user {auth.userId}</Text>\n : <Text>Please sign in</Text>\n}\n```\n\n\n### useApp\n\nRead-only app metadata.\n\n**Signature:** `useApp(): AppInfo`\n\n**Returns:** `AppInfo`\n\n| Property | Type | Description |\n|----------|------|-------------|\n| appId | `string` | appId |\n| appName | `string` | appName |\n| appDescription | `string` | appDescription |\n| appIcon | `string` | appIcon |\n\n**Example:**\n```tsx\nconst app = useApp();\n\n// In JSX:\n<Heading>{app.appName}</Heading>\n{app.appDescription && <Text>{app.appDescription}</Text>}\n```\n\n\n### useRender\n\nRead-only render context with connection status.\n\n**Signature:** `useRender(): GguiSessionInfo`\n\n**Returns:** `GguiSessionInfo`\n\n| Property | Type | Description |\n|----------|------|-------------|\n| sessionId | `string` | sessionId |\n| isConnected | `boolean` | isConnected |\n\n**Example:**\n```tsx\nconst render = useRender();\n\n// In JSX:\n<Badge variant={render.isConnected ? 'success' : 'error'}>\n {render.isConnected ? 'Connected' : 'Disconnected'}\n</Badge>\n```\n\n\n## Provider\n\n\n\nThe provider is set up automatically by the renderer. Generated components do not need to wrap themselves in a provider.\n\n### GguiWireProvider\n\nReact context provider that injects WireConfig for all wire hooks.\n\n**Props:**\n\n| Prop | Type | Description |\n|------|------|-------------|\n| config | `WireConfig` | config |\n| children | `ReactNode` | children |\n\n**Example:**\n```tsx\n<GguiWireProvider config={wireConfig}>\n <YourComponent />\n</GguiWireProvider>\n```\n\n\n## Internal: WireConfig\n\n\n\nThis is the configuration object passed to GguiWireProvider. Generated components do not interact with this directly — it is provided by the renderer.\n\n### WireConfig\n\nConfiguration injected by the provider — the renderer inside the\niframe.\n\nEvery method is typed against the contract generic `T` so typed\ncallers get compile-time enforcement:\n - `dispatch(name, data)` — `name` MUST be a declared actionSpec\n key; `data` MUST satisfy that action's schema.\n - `subscribe(channel, handler)` — same discipline for streamSpec.\n\nUntyped callers (`T = DataContract` default) degrade to the broad\nshape via the conditional `WireDispatchData` / `WireStreamPayload`\naliases — no call-site break.\n\nThe contract's `agentCapabilities.tools` catalog declares tools the AGENT\ninvokes (not the component); user gestures fire via\n`dispatch(name, data)` and the optional `nextStep` field on the\naction entry names the tool the agent SHOULD invoke next.\n\nThe renderer mounts exactly ONE GguiSession per iframe — `render.sessionId`\nis the stable identity the WireProvider was constructed with. No\nper-item scoping factory; with a one-GguiSession-per-mount lifecycle,\n\"scope\" collapses to identity.\n\n| Property | Type | Description |\n|----------|------|-------------|\n| app | `{ readonly appId: string; readonly appName: string; readonly appDescription?: string; readonly appIcon?: string; }` | app |\n| render | `{ readonly sessionId: string; readonly isConnected: boolean; }` | render |\n| auth | `{ readonly userId?: string; readonly isAuthenticated: boolean; }` | auth |\n| dispatch | `<N extends string>( actionName: N, data: WireDispatchData<T, N>, ) => void` | Fire an action to the agent (fire-and-forget over WS). Typed callers get compile-time checked `name` + `data`; untyped callers (`T = DataContract`) keep the broad shape. |\n| subscribe | `<N extends string>( channelName: N, handler: (delivery: StreamDelivery<WireStreamPayload<T, N>>) => void, ) => () => void` | Subscribe to deliveries on a named stream channel. |\n| onDispatchSuppressed | `(info: DispatchSuppressedInfo) => void` | Optional structured observability for the runtime's two dispatch- suppression invariants — the task-scoped duplicate backstop (`reason: 'duplicate-dispatch'`, fired by `useAction`) and the render-lifetime one-shot guard (`reason: 'one-shot-spent'`, fired by the config's own `dispatch` when a declared `oneShot` action is gestured a second time, ggui#1108). Both fire ALONGSIDE the always-on `console.warn`; neither reaches the agent. Reachability constraint: the first-party renderers do NOT set this field — `buildRootWireConfig` (`@ggui-ai/iframe-runtime`) and `<GguiRender>`'s internal config both omit it. It fires only when a host hand-builds a complete `WireConfig` (or passes `onDispatchSuppressed` to `buildWireConfig`). On every first-party render path the `console.warn` is the sole suppression signal — which is what the `oneShot` contract requires as its minimum trace. |\n";
|