@ggui-ai/ui-gen 0.17.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 +449 -194
- 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 +391 -134
- package/dist/advanced/index.js.map +1 -1
- package/dist/boilerplate.js +29 -5
- package/dist/boilerplate.js.map +1 -1
- package/dist/check/index.d.ts +1 -1
- package/dist/check/index.js +70 -0
- 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 +147 -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 +142 -1
- package/dist/evaluation/axis-checks/registry.js.map +1 -1
- package/dist/evaluation/index.d.ts +3 -3
- package/dist/evaluation/index.js +217 -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 +91 -64
- package/dist/harness/index.js.map +1 -1
- package/dist/harness/prompts.js +4 -0
- 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 +31 -7
- package/dist/harness/runtime.js.map +1 -1
- package/dist/harness/types-public.d.ts +8 -1
- package/dist/harness/types-public.js +91 -64
- 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 +391 -134
- 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/free-design-prompt.ts +2 -0
- package/src/boilerplate/generate.ts +8 -1
- package/src/boilerplate/hard-sections.ts +32 -3
- package/src/boilerplate/system-prompt.ts +3 -1
- package/src/boilerplate/templates/layouts/fullscreen-desktop.tsx.tmpl +5 -3
- package/src/boilerplate/templates/layouts/fullscreen-mobile.tsx.tmpl +3 -3
- package/src/boilerplate/templates/layouts/fullscreen-universal.tsx.tmpl +5 -3
- package/src/check/run-tier0.ts +152 -0
- package/src/coding-agent/tools.ts +58 -6
- package/src/contract-context.ts +13 -0
- package/src/create-harness.ts +1 -0
- package/src/evaluation/axis-checks/checks/universal.ts +179 -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
|
@@ -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 {
|
|
@@ -333,6 +334,171 @@ function runViewportSized(input: AxisCheckInput): EvalIssue[] {
|
|
|
333
334
|
];
|
|
334
335
|
}
|
|
335
336
|
|
|
337
|
+
// ── universal.terminal_action_unguarded (ggui#1108, mitigation) ────────
|
|
338
|
+
// A control whose action the user means ONCE — submit, confirm, approve,
|
|
339
|
+
// schedule, reserve, book, pay, checkout, place order — that stays armed after
|
|
340
|
+
// it fires is one double-click away from doing it twice. The check is a WARN,
|
|
341
|
+
// deliberately: terminality lives in the request and the contract, not in a
|
|
342
|
+
// name, so a static rule can suspect it and must not refuse over it. It fires
|
|
343
|
+
// only when a terminal-sounding action or control exists AND the source guards
|
|
344
|
+
// nothing at all (no `disabled=`, no `aria-disabled`), which is the shape with
|
|
345
|
+
// no reading under which the control is safe.
|
|
346
|
+
/** The words that make an action read as meant-once. Matched as WORDS, never as substrings. */
|
|
347
|
+
const TERMINAL_WORDS = new Set([
|
|
348
|
+
"submit", "confirm", "approve", "schedule", "reserve", "book", "pay", "checkout", "purchase", "order",
|
|
349
|
+
]);
|
|
350
|
+
/** In prose (a control's label) the same words, with real boundaries. */
|
|
351
|
+
const TERMINAL_LABEL_RX = /\b(submit|confirm|approve|schedule|reserve|book|pay|checkout|purchase|place\s+order)\b/i;
|
|
352
|
+
const GUARD_RX = /\b(disabled|aria-disabled)\s*=/;
|
|
353
|
+
|
|
354
|
+
/** `submitBooking` → ['submit','booking']; `place_order` → ['place','order']. A NAME is words, not a string to search. */
|
|
355
|
+
function nameWords(name: string): string[] {
|
|
356
|
+
return name
|
|
357
|
+
.split(/(?=[A-Z])|[_\-\s]+/)
|
|
358
|
+
.map((w) => w.toLowerCase())
|
|
359
|
+
.filter((w) => w.length > 0);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/** The terminal-sounding action names and control labels a source carries, deduped, in order. */
|
|
363
|
+
export function findUnguardedTerminalControls(sourceCode: string): string[] {
|
|
364
|
+
if (GUARD_RX.test(sourceCode)) return [];
|
|
365
|
+
const names = new Set<string>();
|
|
366
|
+
for (const m of sourceCode.matchAll(/useAction(?:<[^>]*>)?\(\s*['"`]([^'"`]+)['"`]/g)) {
|
|
367
|
+
const name = m[1] ?? "";
|
|
368
|
+
if (nameWords(name).some((w) => TERMINAL_WORDS.has(w))) names.add(name);
|
|
369
|
+
}
|
|
370
|
+
// A control's own words, taken as the plain text immediately before its close
|
|
371
|
+
// tag — attributes can carry `=>`, so a label is read from the text, never by
|
|
372
|
+
// trying to skip an attribute list with a regex.
|
|
373
|
+
for (const m of sourceCode.matchAll(/>([^<>{}]{1,60})<\/Button>/g)) {
|
|
374
|
+
const label = (m[1] ?? "").replace(/\s+/g, " ").trim();
|
|
375
|
+
if (label.length > 0 && TERMINAL_LABEL_RX.test(label)) names.add(label.slice(0, 40));
|
|
376
|
+
}
|
|
377
|
+
return [...names];
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function runTerminalActionUnguarded(input: AxisCheckInput): EvalIssue[] {
|
|
381
|
+
if (input.compiledCode === null) return [];
|
|
382
|
+
const found = findUnguardedTerminalControls(input.sourceCode);
|
|
383
|
+
if (found.length === 0) return [];
|
|
384
|
+
const list = found.map((t) => `"${t}"`).join(", ");
|
|
385
|
+
return [
|
|
386
|
+
mkIssue(
|
|
387
|
+
"universal.terminal_action_unguarded",
|
|
388
|
+
`${list} reads as an action the user means once, and nothing in this component disables the control after it fires — a double-click sends it twice.`,
|
|
389
|
+
"Hold the fired state (`const [submitted, setSubmitted] = useState(false)`), set it in the handler, and pass `disabled={submitted}` to that control with a word that says so (\"Submitted\"). If the action IS meant to repeat — sending a message, adding an item — leave it armed and ignore this.",
|
|
390
|
+
"warn",
|
|
391
|
+
),
|
|
392
|
+
];
|
|
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
|
+
|
|
336
502
|
export const UNIVERSAL_CHECKS: readonly AxisCheck[] = [
|
|
337
503
|
{
|
|
338
504
|
id: "universal.icon_name_known",
|
|
@@ -352,6 +518,18 @@ export const UNIVERSAL_CHECKS: readonly AxisCheck[] = [
|
|
|
352
518
|
values: ALL_RENDER_VALUES,
|
|
353
519
|
run: runViewportSized,
|
|
354
520
|
},
|
|
521
|
+
{
|
|
522
|
+
id: "universal.root_width_cap",
|
|
523
|
+
axis: "render",
|
|
524
|
+
values: ALL_RENDER_VALUES,
|
|
525
|
+
run: runRootWidthCap,
|
|
526
|
+
},
|
|
527
|
+
{
|
|
528
|
+
id: "universal.terminal_action_unguarded",
|
|
529
|
+
axis: "render",
|
|
530
|
+
values: ALL_RENDER_VALUES,
|
|
531
|
+
run: runTerminalActionUnguarded,
|
|
532
|
+
},
|
|
355
533
|
{
|
|
356
534
|
id: "universal.accent_text_ink",
|
|
357
535
|
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";
|