@ggui-ai/ui-gen 0.18.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/generation-dispatch.d.ts +1 -1
- package/dist/adapters/generation-dispatch.js +197 -86
- package/dist/adapters/generation-dispatch.js.map +1 -1
- package/dist/adapters/index.js +4 -6
- package/dist/adapters/index.js.map +1 -1
- package/dist/advanced/index.d.ts +2 -2
- package/dist/advanced/index.js +131 -22
- package/dist/advanced/index.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 +97 -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 +92 -1
- package/dist/evaluation/axis-checks/registry.js.map +1 -1
- package/dist/evaluation/index.d.ts +3 -3
- package/dist/evaluation/index.js +167 -84
- package/dist/evaluation/index.js.map +1 -1
- package/dist/harness/check/runtime-render/index.d.ts +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 +131 -22
- 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/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 +115 -1
- package/src/evaluation/axis-checks/dispatch.ts +8 -1
- package/src/evaluation/types-public.ts +9 -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
package/dist/workflows.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { C as Classification } from './axes-CzLEMDeB.js';
|
|
|
2
2
|
import { Workflow } from './harness/types-public.js';
|
|
3
3
|
import '@ggui-ai/protocol';
|
|
4
4
|
import './styling-profile-CxfvV48g.js';
|
|
5
|
-
import './types-public-
|
|
5
|
+
import './types-public-DKR_h70H.js';
|
|
6
6
|
import './design-mode-DLufhrNp.js';
|
|
7
7
|
import './types-BOvHNG7K.js';
|
|
8
8
|
import './llm.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ggui-ai/ui-gen",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"description": "UI generation harness for ggui — the LLM system prompt, multi-turn coding loop, and evaluation engine that turn a DataContract into a React component. Implements the UiGenerator contract from @ggui-ai/mcp-server-core; consumed by @ggui-ai/mcp-server.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"keywords": [
|
|
@@ -188,9 +188,9 @@
|
|
|
188
188
|
"puppeteer-core": "^24.40.0 || ^25.0.0",
|
|
189
189
|
"react": "^18.0.0 || ^19.0.0",
|
|
190
190
|
"react-dom": "^18.0.0 || ^19.0.0",
|
|
191
|
-
"@ggui-ai/mcp-server-core": "0.
|
|
192
|
-
"@ggui-ai/project-config": "0.
|
|
193
|
-
"@ggui-ai/protocol": "0.
|
|
191
|
+
"@ggui-ai/mcp-server-core": "0.19.0",
|
|
192
|
+
"@ggui-ai/project-config": "0.19.0",
|
|
193
|
+
"@ggui-ai/protocol": "0.19.0"
|
|
194
194
|
},
|
|
195
195
|
"peerDependenciesMeta": {
|
|
196
196
|
"@google/adk": {
|
|
@@ -229,11 +229,11 @@
|
|
|
229
229
|
"openai": "^7.9.0",
|
|
230
230
|
"typescript": "^5.0.0",
|
|
231
231
|
"zod": "^4.3.6",
|
|
232
|
-
"@ggui-ai/design": "0.
|
|
233
|
-
"@ggui-ai/
|
|
234
|
-
"@ggui-ai/
|
|
235
|
-
"@ggui-ai/wire": "0.
|
|
236
|
-
"@ggui-ai/
|
|
232
|
+
"@ggui-ai/design": "0.19.0",
|
|
233
|
+
"@ggui-ai/gadgets": "0.19.0",
|
|
234
|
+
"@ggui-ai/sandbox": "0.19.0",
|
|
235
|
+
"@ggui-ai/wire": "0.19.0",
|
|
236
|
+
"@ggui-ai/ui-visual-tester": "0.19.0"
|
|
237
237
|
},
|
|
238
238
|
"devDependencies": {
|
|
239
239
|
"@google/adk": "^2.0.0",
|
|
@@ -248,9 +248,9 @@
|
|
|
248
248
|
"react-dom": "^19.0.0",
|
|
249
249
|
"tsup": "^8.5.0",
|
|
250
250
|
"vitest": "^3.2.6",
|
|
251
|
-
"@ggui-ai/
|
|
252
|
-
"@ggui-ai/
|
|
253
|
-
"@ggui-ai/
|
|
251
|
+
"@ggui-ai/mcp-server-core": "0.19.0",
|
|
252
|
+
"@ggui-ai/project-config": "0.19.0",
|
|
253
|
+
"@ggui-ai/protocol": "0.19.0"
|
|
254
254
|
},
|
|
255
255
|
"engines": {
|
|
256
256
|
"node": ">=22.0.0"
|
|
@@ -62,15 +62,10 @@ export async function runAdkLoop(
|
|
|
62
62
|
): Promise<AdapterResult> {
|
|
63
63
|
const startTime = Date.now();
|
|
64
64
|
|
|
65
|
-
const { FunctionTool, LlmAgent, InMemoryRunner, isFinalResponse } = await import('@google/adk');
|
|
65
|
+
const { FunctionTool, Gemini, LlmAgent, InMemoryRunner, isFinalResponse } = await import('@google/adk');
|
|
66
66
|
|
|
67
67
|
const capture = createCapture();
|
|
68
68
|
|
|
69
|
-
// Ensure ADK can find the API key
|
|
70
|
-
if (params.apiKey && !process.env.GOOGLE_GENAI_API_KEY) {
|
|
71
|
-
process.env.GOOGLE_GENAI_API_KEY = params.apiKey;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
69
|
// Build tools with capture wrappers
|
|
75
70
|
const adkTools = params.tools.map((def) =>
|
|
76
71
|
new FunctionTool({
|
|
@@ -88,9 +83,18 @@ export async function runAdkLoop(
|
|
|
88
83
|
}),
|
|
89
84
|
);
|
|
90
85
|
|
|
86
|
+
// The key the caller passed WINS, handed to the model directly (ggui#1132).
|
|
87
|
+
// The ADK resolves GOOGLE_GENAI_API_KEY || GOOGLE_API_KEY || GEMINI_API_KEY
|
|
88
|
+
// on its own when no key is given, so writing GOOGLE_GENAI_API_KEY here —
|
|
89
|
+
// a variable this adapter does not own — let a stale ambient value beat a
|
|
90
|
+
// fresh key, and no rotation could reach it. Precedence is fixed at the
|
|
91
|
+
// source: an explicit key never touches process.env; no key defers to the
|
|
92
|
+
// ADK's order untouched.
|
|
93
|
+
const model = params.apiKey ? new Gemini({ model: params.model, apiKey: params.apiKey }) : params.model;
|
|
94
|
+
|
|
91
95
|
const agent = new LlmAgent({
|
|
92
96
|
name: 'ui_generator',
|
|
93
|
-
model
|
|
97
|
+
model,
|
|
94
98
|
instruction: params.systemPrompt,
|
|
95
99
|
tools: adkTools,
|
|
96
100
|
});
|
|
@@ -213,7 +213,7 @@ Rules:
|
|
|
213
213
|
1. All request-specific data → props with defaults. City names, tickers, user names, dates, counts.
|
|
214
214
|
2. Layout and styling are universal. Colors, spacing, structure — these are the reusable part.
|
|
215
215
|
3. Default values come from the current request — so the component renders correctly standalone.
|
|
216
|
-
4. Copy comes from the props. NEVER invent display text the contract does not carry — no eyebrow / kicker / overline (a \`<Text caps>\` label), no helper line, no status text, no tagline.
|
|
216
|
+
4. Copy comes from the props. NEVER invent display text the contract does not carry — no eyebrow / kicker / overline (a \`<Text caps>\` label), no helper line, no status text, no tagline. **Do NOT add a caps label above the heading** — not \`<Text caps>LET'S BEGIN</Text>\`, not \`WELCOME\`, not a section kicker like \`GETTING STARTED\` or \`OVERVIEW\` — UNLESS a prop supplies that exact text (\`<Text caps>{props.section}</Text>\`). When the request asks for a heading and a line, render the heading and the line and NOTHING above the heading. A heading's fallback is the request's own words, never a slogan of your own. Text the agent cannot change is chatter on every render, and a caps eyebrow you wrote is the most common form of it.
|
|
217
217
|
5. Props interface must be typed and exported.`;
|
|
218
218
|
|
|
219
219
|
/**
|
|
@@ -603,7 +603,7 @@ Don't render text-only metrics. A weather widget without a sun/cloud, a stock ca
|
|
|
603
603
|
</Row>
|
|
604
604
|
\`\`\`
|
|
605
605
|
|
|
606
|
-
Use \`<Icon name="..." />\` for line icons — ONLY the curated
|
|
606
|
+
Use \`<Icon name="..." />\` for line icons — ONLY the design system's curated set renders (~200 kebab-case Lucide names); a name OUTSIDE it renders an empty box and fails the check. On a single-pass render you cannot look the set up, so use ONLY a name you are sure is in it. Safe common names: \`check\` \`x\` \`plus\` \`minus\` \`search\` \`settings\` \`user\` \`users\` \`calendar\` \`clock\` \`arrow-right\` \`arrow-left\` \`chevron-right\` \`chevron-down\` \`trash-2\` \`edit\` \`info\` \`alert-circle\` \`bell\` \`home\` \`file\` \`file-text\` \`star\` \`heart\` \`mail\` \`send\` \`download\` \`upload\` \`external-link\` \`filter\` \`check-circle\`. The numbered variants (\`edit-2\`, \`edit-3\`) and synonyms (\`pencil\`, \`pen\`, \`trash\`, \`x-circle\`) are NOT in the set — use \`edit\` and \`trash-2\`. If the icon you want is not clearly one of these, OMIT it: a card with no icon beats one with an empty box. Emoji render directly for status/weather/mood. For per-stat tiny accents, use a small icon next to the label.
|
|
607
607
|
|
|
608
608
|
### Spacing — generosity beats compactness
|
|
609
609
|
|
|
@@ -12,6 +12,10 @@ import * as esbuild from 'esbuild';
|
|
|
12
12
|
import { AgentWorkspace } from './workspace';
|
|
13
13
|
import { getSoftWarnings } from './self-check';
|
|
14
14
|
import { runTier0Checks } from '../check/index.js';
|
|
15
|
+
import { runGatedAxisChecks, traceAxisChecksSkipped } from '../evaluation/axis-checks/dispatch.js';
|
|
16
|
+
import type { AxisCheck, AxisCheckInput } from '../evaluation/types-public.js';
|
|
17
|
+
import type { Classification } from '../classifier/axes.js';
|
|
18
|
+
import type { CanvasClass } from '../design-mode.js';
|
|
15
19
|
import { PRIMITIVES_DOCUMENTATION } from '../validation/index.js';
|
|
16
20
|
import type { DataContract } from '@ggui-ai/protocol';
|
|
17
21
|
import type {
|
|
@@ -46,6 +50,22 @@ function getComponentDocumentation(name: string): string {
|
|
|
46
50
|
// Auto-commit helper (shared by write and apply_changes)
|
|
47
51
|
// =============================================================================
|
|
48
52
|
|
|
53
|
+
/**
|
|
54
|
+
* The deterministic axis family at auto-commit (ggui#1122) — what a caller with a
|
|
55
|
+
* harness hands `executeTool` so the checks run on EVERY commit the coding agent
|
|
56
|
+
* makes, i.e. on the serve path, not only inside the eval round a serving deployment
|
|
57
|
+
* never enters. `checks` is the harness's pre-filtered list (`harness.check.axisChecks`)
|
|
58
|
+
* — the same list `run-check.ts` feeds the same pure runner. Absent: nothing runs.
|
|
59
|
+
*/
|
|
60
|
+
export interface AutoCommitAxisChecks {
|
|
61
|
+
readonly checks: readonly AxisCheck[];
|
|
62
|
+
readonly classification: Classification;
|
|
63
|
+
/** The ORIGINAL request — several checks read it (labels invented past the copy, terminal words). */
|
|
64
|
+
readonly originalPrompt: string;
|
|
65
|
+
/** The canvas the harness was built for (ggui#1117); absent = no rendering context, never a default. */
|
|
66
|
+
readonly canvas?: CanvasClass;
|
|
67
|
+
}
|
|
68
|
+
|
|
49
69
|
async function autoCommit(
|
|
50
70
|
workspace: AgentWorkspace,
|
|
51
71
|
commitMeta: Map<string, CommitMetadata>,
|
|
@@ -72,6 +92,8 @@ async function autoCommit(
|
|
|
72
92
|
gadgetTypes?: Readonly<Record<string, string>>,
|
|
73
93
|
/** Which tier-0 legs fire — see `runTier0Checks`'s `designMode`. */
|
|
74
94
|
designMode: DesignMode = DEFAULT_DESIGN_MODE,
|
|
95
|
+
/** ggui#1122: run the harness's axis checks here too; absent, none run. */
|
|
96
|
+
axis?: AutoCommitAxisChecks,
|
|
75
97
|
): Promise<ToolResult> {
|
|
76
98
|
const commitStart = Date.now();
|
|
77
99
|
const raw = workspace.read();
|
|
@@ -123,17 +145,45 @@ async function autoCommit(
|
|
|
123
145
|
gadgetTypes,
|
|
124
146
|
designMode,
|
|
125
147
|
);
|
|
126
|
-
|
|
148
|
+
// ggui#1122: the deterministic axis family runs HERE — on every commit the coding
|
|
149
|
+
// agent makes, which is the serve path — through the one pure runner `run-check.ts`
|
|
150
|
+
// feeds (the ggui#1046 trace line prints on both paths). Posture is the founder's:
|
|
151
|
+
// warn-as-warn, fail-as-fail. A WARN that started blocking a served generation
|
|
152
|
+
// because it moved surface would be a posture change, not a relocation, and comes
|
|
153
|
+
// to him as a named item. Only a source that BUILT is judged: on a build failure
|
|
154
|
+
// the checks are not asked (the same stand-down the gate+run wrapper makes) and
|
|
155
|
+
// the ggui#1046 trace says so — a skipped round never reads as a clean one.
|
|
156
|
+
const axisFacts = axis !== undefined
|
|
157
|
+
? {
|
|
158
|
+
sourceCode: formatted,
|
|
159
|
+
...(contract !== undefined ? { contract } : {}),
|
|
160
|
+
originalPrompt: axis.originalPrompt,
|
|
161
|
+
classification: axis.classification,
|
|
162
|
+
designMode,
|
|
163
|
+
...(axis.canvas !== undefined ? { canvas: axis.canvas } : {}),
|
|
164
|
+
}
|
|
165
|
+
: undefined;
|
|
166
|
+
if (axisFacts !== undefined && !buildSuccess) traceAxisChecksSkipped(axisFacts, 'build failed — no check ran');
|
|
167
|
+
const axisRun = axis !== undefined && axisFacts !== undefined && buildSuccess
|
|
168
|
+
? runGatedAxisChecks(axis.checks, { ...axisFacts, compiledCode } satisfies AxisCheckInput)
|
|
169
|
+
: undefined;
|
|
170
|
+
const axisIssues = axisRun?.issues ?? [];
|
|
171
|
+
const checkIssues = [...tier0Issues, ...axisIssues];
|
|
172
|
+
const tier0Fails = checkIssues.filter(i => i.result === 'fail');
|
|
127
173
|
const selfCheckPassed = tier0Fails.length === 0;
|
|
128
174
|
// When contextPolicy.labeledTier0 is set, prefix each violation with
|
|
129
175
|
// its P0/P1/P2 priority so the LLM can rank against the prompt's
|
|
130
176
|
// schema. The default (off) emits unlabeled feedback.
|
|
131
|
-
const violations =
|
|
177
|
+
const violations = checkIssues
|
|
132
178
|
.filter(i => i.result === 'fail')
|
|
133
179
|
.map(i => contextPolicy?.labeledTier0
|
|
134
180
|
? `[${i.priority ?? 'P0'}-${i.category}] ${i.description}\n Fix: ${i.fix}`
|
|
135
181
|
: `[${i.category}] ${i.description}\n Fix: ${i.fix}`);
|
|
136
|
-
|
|
182
|
+
// Axis WARNs ride the existing non-blocking channel — feedback the model sees, never a block.
|
|
183
|
+
const axisWarnings = axisIssues
|
|
184
|
+
.filter(i => i.result === 'warn')
|
|
185
|
+
.map(i => `[${i.subcategory}] ${i.description} Fix: ${i.fix}`);
|
|
186
|
+
const softWarnings = [...getSoftWarnings(raw), ...axisWarnings];
|
|
137
187
|
const selfCheckMs = Date.now() - selfCheckStart;
|
|
138
188
|
|
|
139
189
|
// Always commit (preserves history)
|
|
@@ -144,7 +194,7 @@ async function autoCommit(
|
|
|
144
194
|
|
|
145
195
|
const status = buildSuccess && selfCheckPassed ? 'PASS' : 'FAIL';
|
|
146
196
|
console.log(
|
|
147
|
-
`[coding-agent] auto-commit: ${status} | build=${buildMs}ms self-check=${selfCheckMs}ms git=${gitMs}ms total=${Date.now() - commitStart}ms | violations=${violations.length}`,
|
|
197
|
+
`[coding-agent] auto-commit: ${status} | build=${buildMs}ms self-check=${selfCheckMs}ms git=${gitMs}ms total=${Date.now() - commitStart}ms | violations=${violations.length} | axis=${axisRun === undefined ? 'off' : `${axisIssues.length - axisWarnings.length}F/${axisWarnings.length}W`}`,
|
|
148
198
|
);
|
|
149
199
|
if (violations.length > 0) {
|
|
150
200
|
for (const v of violations) {
|
|
@@ -202,6 +252,8 @@ export async function executeTool(
|
|
|
202
252
|
gadgetTypes?: Readonly<Record<string, string>>,
|
|
203
253
|
/** Which tier-0 legs fire on auto-commit — see `runTier0Checks`'s `designMode`. */
|
|
204
254
|
designMode: DesignMode = DEFAULT_DESIGN_MODE,
|
|
255
|
+
/** ggui#1122: the harness's axis checks, run at every auto-commit — see `AutoCommitAxisChecks`. */
|
|
256
|
+
axis?: AutoCommitAxisChecks,
|
|
205
257
|
): Promise<ToolResult> {
|
|
206
258
|
switch (tool) {
|
|
207
259
|
case 'write':
|
|
@@ -222,7 +274,7 @@ export async function executeTool(
|
|
|
222
274
|
const message = (input.commit_message as string) || `write ${lineCount} lines`;
|
|
223
275
|
|
|
224
276
|
console.log(`[coding-agent] write: ${lineCount} lines → auto-commit`);
|
|
225
|
-
return autoCommit(workspace, commitMeta, message, contract, contextPolicy, gadgetTypes, designMode);
|
|
277
|
+
return autoCommit(workspace, commitMeta, message, contract, contextPolicy, gadgetTypes, designMode, axis);
|
|
226
278
|
}
|
|
227
279
|
|
|
228
280
|
case 'apply_changes': {
|
|
@@ -529,7 +581,7 @@ export async function executeTool(
|
|
|
529
581
|
const message = (input.commit_message as string) || 'apply changes';
|
|
530
582
|
|
|
531
583
|
console.log(`[coding-agent] apply_changes: ${changes.length} changes applied → auto-commit`);
|
|
532
|
-
return autoCommit(workspace, commitMeta, message, contract, contextPolicy, gadgetTypes, designMode);
|
|
584
|
+
return autoCommit(workspace, commitMeta, message, contract, contextPolicy, gadgetTypes, designMode, axis);
|
|
533
585
|
}
|
|
534
586
|
|
|
535
587
|
case 'get_components_info': {
|
package/src/contract-context.ts
CHANGED
|
@@ -213,7 +213,7 @@ export function buildContractsContext(
|
|
|
213
213
|
// model reads it that way. Emitted only when true, so an absent flag
|
|
214
214
|
// leaves the prompt bytes identical to before this line existed.
|
|
215
215
|
if (entry.confirm === true) {
|
|
216
|
-
line += `\n Author flag: \`confirm\` — the author marked this action
|
|
216
|
+
line += `\n Author flag: \`confirm\` — the author marked this action as one to confirm with the user before it fires.`;
|
|
217
217
|
}
|
|
218
218
|
return line;
|
|
219
219
|
})
|
package/src/create-harness.ts
CHANGED
|
@@ -224,6 +224,7 @@ export function createHarness(input: CreateHarnessInput): Harness {
|
|
|
224
224
|
name,
|
|
225
225
|
classification,
|
|
226
226
|
designMode,
|
|
227
|
+
...(input.canvas !== undefined ? { canvas: input.canvas } : {}),
|
|
227
228
|
...(input.profile !== undefined ? { profile: input.profile } : {}),
|
|
228
229
|
how,
|
|
229
230
|
what,
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
// component. Dispatched via a "universal" gate that matches every render
|
|
5
5
|
// value.
|
|
6
6
|
|
|
7
|
-
import { LUCIDE_ICON_NAMES } from "@ggui-ai/design";
|
|
7
|
+
import { LUCIDE_ICON_NAMES, maxWidth as CONTAINER_MAX_WIDTH } from "@ggui-ai/design";
|
|
8
|
+
import { CANVAS_VIEWPORTS, type CanvasClass } from "../../../design-mode.js";
|
|
8
9
|
import type { EvalIssue } from "../../types-public.js";
|
|
9
10
|
import type { AxisCheck, AxisCheckInput } from "../types.js";
|
|
10
11
|
import {
|
|
@@ -391,6 +392,113 @@ function runTerminalActionUnguarded(input: AxisCheckInput): EvalIssue[] {
|
|
|
391
392
|
];
|
|
392
393
|
}
|
|
393
394
|
|
|
395
|
+
// ── universal.root_width_cap (ggui#1117 — the eval leg ggui#1113 left out) ─
|
|
396
|
+
// FRAME_SIZING's width half: the outermost element FILLS the frame; a width
|
|
397
|
+
// cap belongs INSIDE it, on the column that holds text. A cap on the root is
|
|
398
|
+
// the defect on a wide canvas (the card becomes a narrow strip with the
|
|
399
|
+
// frame's ground on either side) and a no-op in a chat bubble (~400 px; a
|
|
400
|
+
// 480 px cap never binds) — so the check reads `input.canvas`: it stands
|
|
401
|
+
// down with no canvas (never a guess) and below `md`, and fires only for a
|
|
402
|
+
// cap SMALLER than the canvas. A bare `<Container>` is a cap too — its
|
|
403
|
+
// default preset is `lg`, 768 px — so it binds on `lg` / `xl` and not on
|
|
404
|
+
// `md`. A cap the check cannot resolve to pixels (a `max-w-*` class, `rem`,
|
|
405
|
+
// `%`) is reported because the prompt forbids it on the root. WARN, not
|
|
406
|
+
// fail: the prompt names the one shape never to write; whether a centred
|
|
407
|
+
// column was the right composition stays the model's call.
|
|
408
|
+
export interface RootWidthCap {
|
|
409
|
+
/** The outermost element's tag name as written. */
|
|
410
|
+
readonly element: string;
|
|
411
|
+
/** The cap as written (`maxWidth="sm"`, `maxWidth: '480px'`, `max-w-md`, or `lg (Container default)`). */
|
|
412
|
+
readonly cap: string;
|
|
413
|
+
/** The cap in pixels when it resolves (a Container preset, an `Npx` value); `null` otherwise. */
|
|
414
|
+
readonly px: number | null;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/** The opening tag of the OUTERMOST element the default export returns, as written; `null` for none or a fragment. */
|
|
418
|
+
function rootOpeningTag(sourceCode: string): { readonly name: string; readonly attrs: string } | null {
|
|
419
|
+
const start = sourceCode.indexOf("export default function");
|
|
420
|
+
const body = start >= 0 ? sourceCode.slice(start) : sourceCode;
|
|
421
|
+
const ret = /\breturn\s*\(?\s*</.exec(body);
|
|
422
|
+
if (ret === null) return null;
|
|
423
|
+
const at = ret.index + ret[0].length - 1;
|
|
424
|
+
const nameMatch = /^<([A-Za-z][\w.]*)/.exec(body.slice(at));
|
|
425
|
+
if (nameMatch === null) return null;
|
|
426
|
+
let j = at + nameMatch[0].length;
|
|
427
|
+
let depth = 0;
|
|
428
|
+
let quote: string | null = null;
|
|
429
|
+
for (; j < body.length; j++) {
|
|
430
|
+
const ch = body[j]!;
|
|
431
|
+
if (quote !== null) {
|
|
432
|
+
if (ch === quote) quote = null;
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
if (ch === '"' || ch === "'" || ch === "`") quote = ch;
|
|
436
|
+
else if (ch === "{") depth++;
|
|
437
|
+
else if (ch === "}") depth--;
|
|
438
|
+
else if (ch === ">" && depth === 0) break;
|
|
439
|
+
}
|
|
440
|
+
return { name: nameMatch[1]!, attrs: body.slice(at + nameMatch[0].length, j) };
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const CONTAINER_PRESETS: Readonly<Record<string, string>> = CONTAINER_MAX_WIDTH;
|
|
444
|
+
const NOT_A_CAP = new Set(["full", "none", "100%", "unset", "initial", "inherit"]);
|
|
445
|
+
|
|
446
|
+
/** `sm` → 480; `480px` → 480; `100%` / `full` → not a cap (`undefined`); anything else → a cap of unknown size (`null`). */
|
|
447
|
+
function capPx(value: string): number | null | undefined {
|
|
448
|
+
const v = value.trim();
|
|
449
|
+
if (NOT_A_CAP.has(v)) return undefined;
|
|
450
|
+
const resolved = CONTAINER_PRESETS[v] ?? v;
|
|
451
|
+
if (NOT_A_CAP.has(resolved)) return undefined;
|
|
452
|
+
const px = /^(\d+(?:\.\d+)?)px$/.exec(resolved);
|
|
453
|
+
return px === null ? null : Number(px[1]);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/** The width cap the outermost returned element carries, or `null` when it fills the frame. */
|
|
457
|
+
export function findRootWidthCap(sourceCode: string): RootWidthCap | null {
|
|
458
|
+
const root = rootOpeningTag(sourceCode);
|
|
459
|
+
if (root === null) return null;
|
|
460
|
+
const { name, attrs } = root;
|
|
461
|
+
const prop = /\bmaxWidth\s*=\s*(?:"([^"]*)"|'([^']*)'|\{\s*(?:"([^"]*)"|'([^']*)'|(\d+(?:\.\d+)?))\s*\})/.exec(attrs);
|
|
462
|
+
if (prop !== null) {
|
|
463
|
+
const raw = prop[1] ?? prop[2] ?? prop[3] ?? prop[4] ?? `${prop[5]!}px`;
|
|
464
|
+
const px = capPx(raw);
|
|
465
|
+
return px === undefined ? null : { element: name, cap: `maxWidth="${raw}"`, px };
|
|
466
|
+
}
|
|
467
|
+
const style = /\bmaxWidth\s*:\s*(?:"([^"]*)"|'([^']*)'|`([^`]*)`|(\d+(?:\.\d+)?))/.exec(attrs);
|
|
468
|
+
if (style !== null) {
|
|
469
|
+
const raw = style[1] ?? style[2] ?? style[3] ?? `${style[4]!}px`;
|
|
470
|
+
const px = capPx(raw);
|
|
471
|
+
return px === undefined ? null : { element: name, cap: `maxWidth: '${raw}'`, px };
|
|
472
|
+
}
|
|
473
|
+
const cls = /(?<![\w-])max-w-([\w[\]%.-]+)/.exec(attrs);
|
|
474
|
+
if (cls !== null) {
|
|
475
|
+
if (cls[1] === "full" || cls[1] === "none") return null;
|
|
476
|
+
return { element: name, cap: `max-w-${cls[1]!}`, px: null };
|
|
477
|
+
}
|
|
478
|
+
if (name === "Container") return { element: name, cap: "lg (Container default)", px: capPx("lg") ?? null };
|
|
479
|
+
return null;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const WIDE_CANVASES: ReadonlySet<CanvasClass> = new Set<CanvasClass>(["md", "lg", "xl"]);
|
|
483
|
+
|
|
484
|
+
function runRootWidthCap(input: AxisCheckInput): EvalIssue[] {
|
|
485
|
+
if (input.compiledCode === null) return [];
|
|
486
|
+
if (input.canvas === undefined || !WIDE_CANVASES.has(input.canvas)) return [];
|
|
487
|
+
const cap = findRootWidthCap(input.sourceCode);
|
|
488
|
+
if (cap === null) return [];
|
|
489
|
+
const width = CANVAS_VIEWPORTS[input.canvas].width;
|
|
490
|
+
if (cap.px !== null && cap.px >= width) return [];
|
|
491
|
+
const sized = cap.px !== null ? `${cap.px}px on a ${width}px canvas` : `on a ${width}px canvas`;
|
|
492
|
+
return [
|
|
493
|
+
mkIssue(
|
|
494
|
+
"universal.root_width_cap",
|
|
495
|
+
`The outermost element <${cap.element}> carries a width cap (${cap.cap}, ${sized}): on this canvas the card becomes a narrow strip with the frame's ground on either side. The frame owns the width; a cap belongs INSIDE the root, on the column that holds text.`,
|
|
496
|
+
'Let the root fill the frame — drop the cap on the element you return at the top (or `maxWidth="full"`) — and put the reading measure on an inner column: `<Box padding="lg"><Container maxWidth="sm">…</Container></Box>`.',
|
|
497
|
+
"warn",
|
|
498
|
+
),
|
|
499
|
+
];
|
|
500
|
+
}
|
|
501
|
+
|
|
394
502
|
export const UNIVERSAL_CHECKS: readonly AxisCheck[] = [
|
|
395
503
|
{
|
|
396
504
|
id: "universal.icon_name_known",
|
|
@@ -410,6 +518,12 @@ export const UNIVERSAL_CHECKS: readonly AxisCheck[] = [
|
|
|
410
518
|
values: ALL_RENDER_VALUES,
|
|
411
519
|
run: runViewportSized,
|
|
412
520
|
},
|
|
521
|
+
{
|
|
522
|
+
id: "universal.root_width_cap",
|
|
523
|
+
axis: "render",
|
|
524
|
+
values: ALL_RENDER_VALUES,
|
|
525
|
+
run: runRootWidthCap,
|
|
526
|
+
},
|
|
413
527
|
{
|
|
414
528
|
id: "universal.terminal_action_unguarded",
|
|
415
529
|
axis: "render",
|
|
@@ -9,7 +9,7 @@ import { REGISTRY } from "./registry.js";
|
|
|
9
9
|
import { matches, type AxisCheck, type AxisCheckInput } from "./types.js";
|
|
10
10
|
import { axisCheckTraceEnabled } from "./helpers.js";
|
|
11
11
|
import { createHash } from "node:crypto";
|
|
12
|
-
import type { DesignMode } from "../../design-mode.js";
|
|
12
|
+
import type { CanvasClass, DesignMode } from "../../design-mode.js";
|
|
13
13
|
|
|
14
14
|
export interface RunAxisChecksInput {
|
|
15
15
|
sourceCode: string;
|
|
@@ -18,6 +18,8 @@ export interface RunAxisChecksInput {
|
|
|
18
18
|
originalPrompt: string;
|
|
19
19
|
/** Which triad produced the source — see `AxisCheckInput.designMode`. */
|
|
20
20
|
designMode?: DesignMode;
|
|
21
|
+
/** The canvas the source is judged for — see `AxisCheckInput.canvas` (ggui#1117); absent = no rendering context. */
|
|
22
|
+
canvas?: CanvasClass;
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
export function runAxisChecks(
|
|
@@ -30,6 +32,7 @@ export function runAxisChecks(
|
|
|
30
32
|
originalPrompt: input.originalPrompt,
|
|
31
33
|
classification,
|
|
32
34
|
...(input.designMode !== undefined ? { designMode: input.designMode } : {}),
|
|
35
|
+
...(input.canvas !== undefined ? { canvas: input.canvas } : {}),
|
|
33
36
|
};
|
|
34
37
|
if (input.compiledCode === null) {
|
|
35
38
|
traceAxisChecksSkipped(facts, "compiledCode null — no check ran");
|
|
@@ -93,6 +96,8 @@ export interface AxisCheckTrace {
|
|
|
93
96
|
readonly classification: Classification["vector"];
|
|
94
97
|
readonly propsSpecKeys: readonly string[];
|
|
95
98
|
readonly propsSpecPropertyKeys: readonly string[];
|
|
99
|
+
/** The canvas the verdict was built for (ggui#1117) — present only when the input carried one. */
|
|
100
|
+
readonly canvas?: CanvasClass;
|
|
96
101
|
readonly matched: readonly string[];
|
|
97
102
|
readonly issues: ReadonlyArray<{ readonly id: string; readonly result: EvalIssue["result"] }>;
|
|
98
103
|
readonly note?: string;
|
|
@@ -107,5 +112,7 @@ function traceFacts(input: AxisTraceFacts): Omit<AxisCheckTrace, "matched" | "is
|
|
|
107
112
|
classification: input.classification.vector,
|
|
108
113
|
propsSpecKeys: Object.keys(propsSpec ?? {}),
|
|
109
114
|
propsSpecPropertyKeys: Object.keys(propsSpec?.properties ?? {}),
|
|
115
|
+
// No default here, unlike designMode: an absent canvas is a fact about the caller, not a guess to paper over.
|
|
116
|
+
...(input.canvas !== undefined ? { canvas: input.canvas } : {}),
|
|
110
117
|
};
|
|
111
118
|
}
|
|
@@ -280,6 +280,15 @@ export interface AxisCheckInput {
|
|
|
280
280
|
* means `constrained`.
|
|
281
281
|
*/
|
|
282
282
|
designMode?: DesignMode;
|
|
283
|
+
/**
|
|
284
|
+
* The rendering canvas the source is judged FOR (ggui#1117). A width cap on
|
|
285
|
+
* the outermost element is a defect on a fullscreen canvas and a no-op in a
|
|
286
|
+
* chat bubble, so a check that reads width must know the surface. Populated
|
|
287
|
+
* where the harness is built (`Harness.canvas`); ABSENT for callers with no
|
|
288
|
+
* rendering context — a check handles `undefined`, it never assumes a
|
|
289
|
+
* default (a silent `lg` would be the same guess with a nicer face).
|
|
290
|
+
*/
|
|
291
|
+
canvas?: CanvasClass;
|
|
283
292
|
}
|
|
284
293
|
|
|
285
294
|
/**
|
|
@@ -178,6 +178,7 @@ export function createGenerateTaskRunner(input: CreateGenerateRunnerInput): Task
|
|
|
178
178
|
systemPrompt: session.systemPrompt,
|
|
179
179
|
harness: session.harness,
|
|
180
180
|
contract: params.contract,
|
|
181
|
+
originalPrompt: params.userPrompt,
|
|
181
182
|
commitMeta: session.commitMeta,
|
|
182
183
|
originalProps: session.originalProps,
|
|
183
184
|
costTracker: session.costTracker,
|
|
@@ -155,6 +155,13 @@ export interface CodingTurnContext {
|
|
|
155
155
|
readonly systemPrompt: string;
|
|
156
156
|
readonly harness: Harness;
|
|
157
157
|
readonly contract: DataContract | undefined;
|
|
158
|
+
/**
|
|
159
|
+
* The ORIGINAL request (ggui#1122) — what the eval round has always judged
|
|
160
|
+
* against, now handed to auto-commit so the axis checks run on the serve
|
|
161
|
+
* path with the same input. Required: a caller that forgot it would silently
|
|
162
|
+
* run no axis checks on every served generation.
|
|
163
|
+
*/
|
|
164
|
+
readonly originalPrompt: string;
|
|
158
165
|
readonly commitMeta: Map<string, CommitMetadata>;
|
|
159
166
|
readonly originalProps: string | undefined;
|
|
160
167
|
readonly costTracker: CostTracker | null;
|
|
@@ -669,6 +676,13 @@ ${closingInstruction}`;
|
|
|
669
676
|
// Which tier-0 legs fire — the design-vocabulary legs stand down in
|
|
670
677
|
// `free` mode; every contract leg runs in both.
|
|
671
678
|
harness.designMode,
|
|
679
|
+
// ggui#1122: the harness's pre-filtered axis checks run at auto-commit — the serve path.
|
|
680
|
+
{
|
|
681
|
+
checks: harness.check.axisChecks,
|
|
682
|
+
classification: harness.classification,
|
|
683
|
+
originalPrompt: ctx.originalPrompt,
|
|
684
|
+
...(harness.canvas !== undefined ? { canvas: harness.canvas } : {}),
|
|
685
|
+
},
|
|
672
686
|
);
|
|
673
687
|
const toolMs = Date.now() - toolStart;
|
|
674
688
|
|
|
@@ -303,6 +303,13 @@ export interface Harness {
|
|
|
303
303
|
* into `free`.
|
|
304
304
|
*/
|
|
305
305
|
readonly designMode: DesignMode;
|
|
306
|
+
/**
|
|
307
|
+
* The rendering canvas this harness was built for (ggui#1117) — threaded
|
|
308
|
+
* from `CreateHarnessInput.canvas` into the HOW leg (the `free` prompt
|
|
309
|
+
* states it) and, via `run-check.ts`, onto every axis check's input.
|
|
310
|
+
* ABSENT when the caller had no rendering context; never defaulted.
|
|
311
|
+
*/
|
|
312
|
+
readonly canvas?: CanvasClass;
|
|
306
313
|
/** The app's generation profile in force for this harness (#991), if any. */
|
|
307
314
|
readonly profile?: GenerationProfileInput;
|
|
308
315
|
|
package/src/run-check.ts
CHANGED
|
@@ -119,6 +119,8 @@ export async function runCheck(input: RunCheckInput): Promise<CheckResult> {
|
|
|
119
119
|
originalPrompt: prompt,
|
|
120
120
|
classification: harness.classification,
|
|
121
121
|
designMode: harness.designMode,
|
|
122
|
+
// ggui#1117: the canvas the harness was built for, when it has one — never a default.
|
|
123
|
+
...(harness.canvas !== undefined ? { canvas: harness.canvas } : {}),
|
|
122
124
|
};
|
|
123
125
|
// The one runner `runAxisChecks` shares (ggui#1046 trace prints here, on the served path).
|
|
124
126
|
const axisRun = runGatedAxisChecks(check.axisChecks, axisInput);
|
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";
|