@ggui-ai/ui-gen 0.19.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/generation-dispatch.js +51 -2
- package/dist/adapters/generation-dispatch.js.map +1 -1
- package/dist/adapters/index.js +35 -10
- package/dist/adapters/index.js.map +1 -1
- package/dist/adapters/types.d.ts +1 -1
- package/dist/adapters/types.js.map +1 -1
- package/dist/advanced/index.js +86 -12
- package/dist/advanced/index.js.map +1 -1
- package/dist/blueprint-validator.js +5 -2
- package/dist/blueprint-validator.js.map +1 -1
- package/dist/evaluation/axis-checks/index.js +46 -0
- package/dist/evaluation/axis-checks/index.js.map +1 -1
- package/dist/evaluation/axis-checks/registry.js +46 -0
- package/dist/evaluation/axis-checks/registry.js.map +1 -1
- package/dist/evaluation/index.js +46 -0
- package/dist/evaluation/index.js.map +1 -1
- package/dist/harness/check/runtime-render/index.js +5 -2
- package/dist/harness/check/runtime-render/index.js.map +1 -1
- package/dist/harness/check/runtime-render/render-check-worker.js +5 -2
- package/dist/harness/check/runtime-render/render-check-worker.js.map +1 -1
- package/dist/harness/index.js.map +1 -1
- package/dist/harness/types-public.js.map +1 -1
- package/dist/index.js +86 -12
- package/dist/index.js.map +1 -1
- package/package.json +12 -12
- package/src/adapters/openai/raw.ts +11 -5
- package/src/adapters/openai/sdk.ts +8 -5
- package/src/adapters/openai/tokens.ts +49 -0
- package/src/adapters/types.ts +1 -1
- package/src/evaluation/axis-checks/checks/universal.ts +92 -0
- package/src/harness/check/runtime-render/render-check.ts +15 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ggui-ai/ui-gen",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.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/
|
|
192
|
-
"@ggui-ai/
|
|
193
|
-
"@ggui-ai/protocol": "0.
|
|
191
|
+
"@ggui-ai/project-config": "0.20.0",
|
|
192
|
+
"@ggui-ai/mcp-server-core": "0.20.0",
|
|
193
|
+
"@ggui-ai/protocol": "0.20.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/
|
|
233
|
-
"@ggui-ai/
|
|
234
|
-
"@ggui-ai/sandbox": "0.
|
|
235
|
-
"@ggui-ai/
|
|
236
|
-
"@ggui-ai/
|
|
232
|
+
"@ggui-ai/gadgets": "0.20.0",
|
|
233
|
+
"@ggui-ai/design": "0.20.0",
|
|
234
|
+
"@ggui-ai/sandbox": "0.20.0",
|
|
235
|
+
"@ggui-ai/ui-visual-tester": "0.20.0",
|
|
236
|
+
"@ggui-ai/wire": "0.20.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/project-config": "0.20.0",
|
|
252
|
+
"@ggui-ai/protocol": "0.20.0",
|
|
253
|
+
"@ggui-ai/mcp-server-core": "0.20.0"
|
|
254
254
|
},
|
|
255
255
|
"engines": {
|
|
256
256
|
"node": ">=22.0.0"
|
|
@@ -10,6 +10,7 @@ import { GeneratorAdapter, hasCredentials } from '../base';
|
|
|
10
10
|
import type { AdapterConfig, GenerateParams } from '../base';
|
|
11
11
|
import type { AdapterResult, ToolDefinition, ProviderName, AdapterMode } from '../types';
|
|
12
12
|
import { zodToJsonSchema } from '../tool-bridge';
|
|
13
|
+
import { splitOpenAiUsage } from './tokens';
|
|
13
14
|
import {
|
|
14
15
|
createCapture,
|
|
15
16
|
captureSourceCode,
|
|
@@ -54,6 +55,9 @@ export class OpenAiRawAdapter extends GeneratorAdapter {
|
|
|
54
55
|
const capture = createCapture();
|
|
55
56
|
let totalInputTokens = 0;
|
|
56
57
|
let totalOutputTokens = 0;
|
|
58
|
+
// OpenAI's input token count INCLUDES cached tokens; track the cached
|
|
59
|
+
// subset so tokens.input reports NON-cached input (ggui#1186).
|
|
60
|
+
let totalCachedInputTokens = 0;
|
|
57
61
|
let turnsUsed = 0;
|
|
58
62
|
let allTextOutput = '';
|
|
59
63
|
|
|
@@ -74,6 +78,8 @@ export class OpenAiRawAdapter extends GeneratorAdapter {
|
|
|
74
78
|
if (response.usage) {
|
|
75
79
|
totalInputTokens += response.usage.input_tokens ?? 0;
|
|
76
80
|
totalOutputTokens += response.usage.output_tokens ?? 0;
|
|
81
|
+
totalCachedInputTokens +=
|
|
82
|
+
response.usage.input_tokens_details?.cached_tokens ?? 0;
|
|
77
83
|
}
|
|
78
84
|
|
|
79
85
|
// Process output items
|
|
@@ -156,11 +162,11 @@ export class OpenAiRawAdapter extends GeneratorAdapter {
|
|
|
156
162
|
sourceCode: capture.sourceCode,
|
|
157
163
|
stream: capture.stream,
|
|
158
164
|
generatorMeta: capture.generatorMeta,
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
165
|
+
...splitOpenAiUsage(
|
|
166
|
+
totalInputTokens,
|
|
167
|
+
totalOutputTokens,
|
|
168
|
+
totalCachedInputTokens,
|
|
169
|
+
),
|
|
164
170
|
generationTimeMs: Date.now() - startTime,
|
|
165
171
|
turnsUsed,
|
|
166
172
|
};
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { GeneratorAdapter, hasCredentials } from '../base';
|
|
7
7
|
import type { AdapterConfig, GenerateParams } from '../base';
|
|
8
8
|
import type { AdapterResult, ProviderName, AdapterMode } from '../types';
|
|
9
|
+
import { splitOpenAiUsage } from './tokens';
|
|
9
10
|
import {
|
|
10
11
|
createCapture,
|
|
11
12
|
captureSourceCode,
|
|
@@ -87,17 +88,19 @@ export class OpenAiSdkAdapter extends GeneratorAdapter {
|
|
|
87
88
|
const usage = result.state.usage;
|
|
88
89
|
const inputTokens = usage.inputTokens;
|
|
89
90
|
const outputTokens = usage.outputTokens;
|
|
91
|
+
// OpenAI's input token count INCLUDES cached tokens; sum the cached
|
|
92
|
+
// subset across per-request details (ggui#1186).
|
|
93
|
+
const cachedInputTokens = (usage.inputTokensDetails ?? []).reduce(
|
|
94
|
+
(sum, details) => sum + (details.cached_tokens ?? 0),
|
|
95
|
+
0,
|
|
96
|
+
);
|
|
90
97
|
|
|
91
98
|
return {
|
|
92
99
|
compiledCode: capture.compiledCode,
|
|
93
100
|
sourceCode: capture.sourceCode,
|
|
94
101
|
stream: capture.stream,
|
|
95
102
|
generatorMeta: capture.generatorMeta,
|
|
96
|
-
|
|
97
|
-
input: inputTokens,
|
|
98
|
-
output: outputTokens,
|
|
99
|
-
total: inputTokens + outputTokens,
|
|
100
|
-
},
|
|
103
|
+
...splitOpenAiUsage(inputTokens, outputTokens, cachedInputTokens),
|
|
101
104
|
generationTimeMs: Date.now() - startTime,
|
|
102
105
|
turnsUsed: result.rawResponses?.length ?? 0,
|
|
103
106
|
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { AdapterResult } from '../types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Split an OpenAI usage report into the shared cross-adapter token
|
|
5
|
+
* convention (ggui#1186).
|
|
6
|
+
*
|
|
7
|
+
* OpenAI reports the cached prompt tokens as a SUBSET already counted
|
|
8
|
+
* inside its input token total (`usage.input_tokens` on the Responses
|
|
9
|
+
* API, `usage.inputTokens` on the Agents SDK, both INCLUDING cached
|
|
10
|
+
* tokens). Anthropic (and OpenRouter's Anthropic passthrough) instead
|
|
11
|
+
* report `input_tokens` EXCLUDING cache reads, with cache reads as a
|
|
12
|
+
* separate field.
|
|
13
|
+
*
|
|
14
|
+
* The whole fleet reads one convention: **`tokens.input` is the
|
|
15
|
+
* NON-cached input; `cacheReadTokens` is the cache-hit subset.** Cost
|
|
16
|
+
* and observability consumers price/aggregate `input` at the full input
|
|
17
|
+
* rate and `cacheReadTokens` at the cache-read rate, so the cached
|
|
18
|
+
* portion must appear in exactly one of the two — never both. Reporting
|
|
19
|
+
* OpenAI's cache-inclusive `input_tokens` as `tokens.input` while also
|
|
20
|
+
* populating `cacheReadTokens` would double-count the cached tokens (it
|
|
21
|
+
* inflates `$/gen` and breaks `render_cache_metric`'s `cacheReadPct`
|
|
22
|
+
* denominator). So the OpenAI adapters subtract the cached subset here.
|
|
23
|
+
*
|
|
24
|
+
* `total` is the full processed footprint (non-cached input + cache
|
|
25
|
+
* reads + output = `inputTokensInclCached + outputTokens`), matching the
|
|
26
|
+
* Claude adapter's `total` (input + cacheCreation + cacheRead + output);
|
|
27
|
+
* OpenAI has no explicit cache-creation step, so there is no
|
|
28
|
+
* `cacheCreationTokens` term.
|
|
29
|
+
*/
|
|
30
|
+
export function splitOpenAiUsage(
|
|
31
|
+
inputTokensInclCached: number,
|
|
32
|
+
outputTokens: number,
|
|
33
|
+
cachedTokens: number,
|
|
34
|
+
): { tokens: AdapterResult['tokens']; cacheReadTokens: number } {
|
|
35
|
+
// Never let a malformed usage report drive input negative.
|
|
36
|
+
const cacheRead = Math.min(
|
|
37
|
+
Math.max(cachedTokens, 0),
|
|
38
|
+
Math.max(inputTokensInclCached, 0),
|
|
39
|
+
);
|
|
40
|
+
const nonCachedInput = Math.max(inputTokensInclCached, 0) - cacheRead;
|
|
41
|
+
return {
|
|
42
|
+
tokens: {
|
|
43
|
+
input: nonCachedInput,
|
|
44
|
+
output: outputTokens,
|
|
45
|
+
total: nonCachedInput + cacheRead + outputTokens,
|
|
46
|
+
},
|
|
47
|
+
cacheReadTokens: cacheRead,
|
|
48
|
+
};
|
|
49
|
+
}
|
package/src/adapters/types.ts
CHANGED
|
@@ -88,7 +88,7 @@ export interface AdapterResult {
|
|
|
88
88
|
subagentsUsed?: string[];
|
|
89
89
|
/** Tokens written to prompt cache (Claude-specific) */
|
|
90
90
|
cacheCreationTokens?: number;
|
|
91
|
-
/** Tokens read from prompt cache (Claude
|
|
91
|
+
/** Tokens read from prompt cache (Claude / OpenRouter / OpenAI). */
|
|
92
92
|
cacheReadTokens?: number;
|
|
93
93
|
/** SDK session ID for evaluation loop resume (Claude SDK-specific) */
|
|
94
94
|
sdkSessionId?: string;
|
|
@@ -8,6 +8,7 @@ import { LUCIDE_ICON_NAMES, maxWidth as CONTAINER_MAX_WIDTH } from "@ggui-ai/des
|
|
|
8
8
|
import { CANVAS_VIEWPORTS, type CanvasClass } from "../../../design-mode.js";
|
|
9
9
|
import type { EvalIssue } from "../../types-public.js";
|
|
10
10
|
import type { AxisCheck, AxisCheckInput } from "../types.js";
|
|
11
|
+
import type { DataContract } from "@ggui-ai/protocol";
|
|
11
12
|
import {
|
|
12
13
|
getRequiredPropNames,
|
|
13
14
|
mkIssue,
|
|
@@ -499,6 +500,91 @@ function runRootWidthCap(input: AxisCheckInput): EvalIssue[] {
|
|
|
499
500
|
];
|
|
500
501
|
}
|
|
501
502
|
|
|
503
|
+
// ── universal.action_label_dropped (ggui#1190 — the transcription-drop guard) ─
|
|
504
|
+
// The GUARD half of #1190: the model transcribes an actionSpec label onto a
|
|
505
|
+
// control but DROPS a special character ("Confirm & Schedule" → "Confirm
|
|
506
|
+
// Schedule"), because the verbatim label is not at the write-site. This check
|
|
507
|
+
// NAMES that class on the bench and guards regression; the fix (carry the
|
|
508
|
+
// verbatim label / render it from data) lands separately, so #1190 stays open.
|
|
509
|
+
// WARN-class, deterministic, and tuned to flag ONLY the high-confidence drop.
|
|
510
|
+
|
|
511
|
+
const LABEL_SPECIAL_CHAR = /[^a-z0-9\s]/i;
|
|
512
|
+
const normText = (s: string): string => s.toLowerCase().replace(/\s+/g, " ").trim();
|
|
513
|
+
const alnumText = (s: string): string => s.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Extract the VISIBLE rendered text of a component — JSX text nodes (with `{…}`
|
|
517
|
+
* expression containers stripped, nesting-aware) plus visible string
|
|
518
|
+
* attributes (aria-label / placeholder / title / alt). Deliberately excludes
|
|
519
|
+
* identifiers and call args: `useAction('confirmSchedule')` and the `const
|
|
520
|
+
* confirmSchedule` binding must NOT count as the label "Confirm & Schedule"
|
|
521
|
+
* appearing, or every action would false-match its own camelCase name.
|
|
522
|
+
*/
|
|
523
|
+
function visibleText(sourceCode: string): string {
|
|
524
|
+
// JSX text nodes: `>text<`. Excluding `>` from the run (`[^<>]`) skips the
|
|
525
|
+
// `=>` arrow-body conflation — an arrow's `>` is followed by code then a
|
|
526
|
+
// `>`, never text then a `<`, so only true tag-close→text→tag runs match,
|
|
527
|
+
// and identifiers (`const confirmSchedule`, `useAction('confirmSchedule')`)
|
|
528
|
+
// are never between `>` and `<`. `{…}` expression children are stripped so an
|
|
529
|
+
// expression's identifiers don't count as visible text.
|
|
530
|
+
const jsx = (sourceCode.match(/>([^<>]*)</g) ?? [])
|
|
531
|
+
.map((m) => m.slice(1, -1).replace(/\{[^}]*\}/g, " "))
|
|
532
|
+
.join(" ");
|
|
533
|
+
let attrs = "";
|
|
534
|
+
const attrRx = /\b(?:aria-label|placeholder|title|alt)\s*=\s*(?:"([^"]*)"|'([^']*)')/gi;
|
|
535
|
+
let m: RegExpExecArray | null;
|
|
536
|
+
while ((m = attrRx.exec(sourceCode)) !== null) attrs += " " + (m[1] ?? m[2] ?? "");
|
|
537
|
+
return jsx + " " + attrs;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Find actionSpec labels whose special-character content was dropped from the
|
|
542
|
+
* generated source. FALSE-POSITIVE-SAFE by construction — flags ONLY when the
|
|
543
|
+
* label's alphanumeric words appear CONTIGUOUSLY in the source but the full
|
|
544
|
+
* label (with its "&", "/", "+" …) does not, i.e. a clean transcription drop.
|
|
545
|
+
* Every ambiguous shape is exempt: labels rendered from data (`.label`
|
|
546
|
+
* anywhere), labels with no special char (nothing to drop), labels present
|
|
547
|
+
* verbatim, entity-encoded `&` (`&` — the words stop being contiguous, so
|
|
548
|
+
* no flag), and very short labels. Presence-ANYWHERE, not wiring-scoped, on
|
|
549
|
+
* purpose: if the label's text appears anywhere we never flag.
|
|
550
|
+
*/
|
|
551
|
+
export function findDroppedActionLabels(
|
|
552
|
+
sourceCode: string,
|
|
553
|
+
contract: DataContract | undefined,
|
|
554
|
+
): Array<{ readonly name: string; readonly label: string }> {
|
|
555
|
+
const actionSpec = contract?.actionSpec;
|
|
556
|
+
if (actionSpec === undefined) return [];
|
|
557
|
+
// Component renders any label from data → it does not transcribe → exempt all.
|
|
558
|
+
if (/\.label\b/.test(sourceCode)) return [];
|
|
559
|
+
const vis = visibleText(sourceCode);
|
|
560
|
+
const normVis = normText(vis);
|
|
561
|
+
const alnumVis = alnumText(vis);
|
|
562
|
+
const out: Array<{ name: string; label: string }> = [];
|
|
563
|
+
for (const [name, entry] of Object.entries(actionSpec)) {
|
|
564
|
+
const label = entry.label;
|
|
565
|
+
if (typeof label !== "string" || label.length === 0 || label === name) continue;
|
|
566
|
+
if (!LABEL_SPECIAL_CHAR.test(label)) continue; // no special char → nothing to drop
|
|
567
|
+
if (alnumText(label).length < 3) continue; // too short to match confidently
|
|
568
|
+
if (normVis.includes(normText(label))) continue; // present verbatim in visible text → correct
|
|
569
|
+
// The label's words appear contiguously in the VISIBLE text but the full
|
|
570
|
+
// label (with its punctuation) does not → a character was dropped.
|
|
571
|
+
if (alnumVis.includes(alnumText(label))) out.push({ name, label });
|
|
572
|
+
}
|
|
573
|
+
return out;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function runActionLabelDropped(input: AxisCheckInput): EvalIssue[] {
|
|
577
|
+
if (input.compiledCode === null) return [];
|
|
578
|
+
return findDroppedActionLabels(input.sourceCode, input.contract).map(({ name, label }) =>
|
|
579
|
+
mkIssue(
|
|
580
|
+
"universal.action_label_dropped",
|
|
581
|
+
`The action "${name}" declares label "${label}", but the rendered control appears to drop its "${label.replace(/[a-z0-9\s]/gi, "").trim()}": the label's words are in the generated source, the full label is not — a transcription drop (ggui#1190).`,
|
|
582
|
+
`Render the label EXACTLY as the contract states it — button text should be the verbatim \`actionSpec.${name}.label\`, or render it from data (\`{x.label}\`), never a re-typed copy that drops "&", "/", "+", etc.`,
|
|
583
|
+
"warn",
|
|
584
|
+
),
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
|
|
502
588
|
export const UNIVERSAL_CHECKS: readonly AxisCheck[] = [
|
|
503
589
|
{
|
|
504
590
|
id: "universal.icon_name_known",
|
|
@@ -524,6 +610,12 @@ export const UNIVERSAL_CHECKS: readonly AxisCheck[] = [
|
|
|
524
610
|
values: ALL_RENDER_VALUES,
|
|
525
611
|
run: runRootWidthCap,
|
|
526
612
|
},
|
|
613
|
+
{
|
|
614
|
+
id: "universal.action_label_dropped",
|
|
615
|
+
axis: "render",
|
|
616
|
+
values: ALL_RENDER_VALUES,
|
|
617
|
+
run: runActionLabelDropped,
|
|
618
|
+
},
|
|
527
619
|
{
|
|
528
620
|
id: "universal.terminal_action_unguarded",
|
|
529
621
|
axis: "render",
|
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
type ParentLink,
|
|
46
46
|
} from "./host-boundary.js";
|
|
47
47
|
import { hostGlobals, openRecord } from "../../../internal/open-record.js";
|
|
48
|
+
import { primeInputs } from "@ggui-ai/ui-visual-tester/prime-inputs";
|
|
48
49
|
|
|
49
50
|
/**
|
|
50
51
|
* Module-namespace-shaped value injected into `loadComponent`'s
|
|
@@ -504,6 +505,20 @@ export async function runRenderCheckInProcess(
|
|
|
504
505
|
let streamsChecked = 0;
|
|
505
506
|
|
|
506
507
|
try {
|
|
508
|
+
// ggui#1187: prime form inputs before the action-wiring probe. A wired
|
|
509
|
+
// Send/Submit gated on a non-empty input (the chat-interface shape) is a
|
|
510
|
+
// no-op on empty, so an un-primed synthetic click never dispatches and
|
|
511
|
+
// the probe false-warns "did not dispatch it". Shared with the visual
|
|
512
|
+
// tester's primeInputs (#1021/#1040) so both prime identically. Priming
|
|
513
|
+
// sets input VALUES (not textContent) and fires input/change before the
|
|
514
|
+
// per-candidate fire-log window is opened, so it neither perturbs the
|
|
515
|
+
// prop-sensitivity text baseline nor false-positives the wiring probe.
|
|
516
|
+
// Best-effort: a priming failure must never abort the check.
|
|
517
|
+
try {
|
|
518
|
+
primeInputs(renderResult.container);
|
|
519
|
+
} catch {
|
|
520
|
+
// Un-primeable DOM falls back to the prior (un-primed) behavior.
|
|
521
|
+
}
|
|
507
522
|
// ── Check 2: Action wiring (BLOCK / unverified-as-warn) ───────────
|
|
508
523
|
if (input.contract?.actionSpec) {
|
|
509
524
|
const actionSpec = input.contract.actionSpec;
|