@ggui-ai/ui-gen 0.21.0 → 0.23.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 +253 -76
- package/dist/adapters/generation-dispatch.js.map +1 -1
- package/dist/adapters/index.js +36 -11
- package/dist/adapters/index.js.map +1 -1
- package/dist/advanced/index.d.ts +3 -3
- package/dist/advanced/index.js +447 -76
- package/dist/advanced/index.js.map +1 -1
- package/dist/blueprint-validator.js +37 -7
- package/dist/blueprint-validator.js.map +1 -1
- package/dist/boilerplate.js +1 -0
- 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-D7kOXNjJ.d.ts → create-ui-generator-D0kbWwln.d.ts} +1 -1
- package/dist/{dispatch-DGvhmARp.d.ts → dispatch-wxaIMwZt.d.ts} +1 -1
- package/dist/evaluation/axis-checks/index.d.ts +3 -3
- package/dist/evaluation/axis-checks/index.js +19 -4
- 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 +19 -4
- package/dist/evaluation/axis-checks/registry.js.map +1 -1
- package/dist/evaluation/index.d.ts +3 -3
- package/dist/evaluation/index.js +58 -26
- package/dist/evaluation/index.js.map +1 -1
- package/dist/harness/check/runtime-render/index.d.ts +4 -4
- package/dist/harness/check/runtime-render/index.js +26 -6
- package/dist/harness/check/runtime-render/index.js.map +1 -1
- package/dist/harness/check/runtime-render/render-check-worker.js.map +1 -1
- package/dist/harness/index.d.ts +1 -1
- package/dist/harness/index.js +37 -11
- 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 +15 -2
- package/dist/harness/result-types.js.map +1 -1
- package/dist/harness/runtime.d.ts +1 -1
- package/dist/harness/runtime.js +2 -1
- package/dist/harness/runtime.js.map +1 -1
- package/dist/harness/types-public.d.ts +9 -5
- package/dist/harness/types-public.js +1 -0
- package/dist/harness/types-public.js.map +1 -1
- package/dist/harness/validator-trace-sink.d.ts +1 -1
- package/dist/index.d.ts +22 -6
- package/dist/index.js +436 -75
- package/dist/index.js.map +1 -1
- package/dist/patch.d.ts +1 -1
- package/dist/{render-check-C0CqVceF.d.ts → render-check-Blwj8gB6.d.ts} +18 -0
- package/dist/tools/get-primitives-ts.d.ts +1 -1
- package/dist/tools/get-primitives-ts.js +1 -1
- package/dist/tools/get-primitives-ts.js.map +1 -1
- package/dist/{types-public-CzYqZclY.d.ts → types-public-Ow-tNtFE.d.ts} +41 -3
- 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/claude/claude-code-login.ts +62 -7
- package/src/adapters/claude/sdk.ts +6 -5
- package/src/advanced/generator.ts +16 -1
- package/src/blueprint-validator.ts +21 -6
- package/src/boilerplate/hard-sections.ts +1 -0
- package/src/boilerplate/templates/layouts/chat-mobile.tsx.tmpl +7 -3
- package/src/boilerplate/templates/layouts/chat-universal.tsx.tmpl +7 -3
- package/src/coding-agent/tools.ts +6 -1
- package/src/contract-context.ts +1 -1
- package/src/create-ui-generator.ts +30 -0
- package/src/evaluation/axis-checks/checks/state-payload.ts +29 -4
- package/src/evaluation/index.ts +1 -0
- package/src/evaluation/llm-evaluator.ts +40 -13
- package/src/evaluation/types-public.ts +42 -2
- package/src/evaluation/visual-evaluator.ts +9 -6
- package/src/generator-build.ts +158 -0
- package/src/harness/check/runtime-render/adapter.ts +19 -0
- package/src/harness/check/runtime-render/render-check-host.ts +31 -9
- package/src/harness/check/runtime-render/render-check.ts +19 -0
- package/src/harness/claude-code-login-query.ts +5 -4
- package/src/harness/coding/assemble-result.ts +8 -1
- package/src/harness/coding/generate-task-runner.ts +75 -36
- package/src/harness/coding/run-coding-turn.ts +9 -0
- package/src/harness/coding/run-eval-round.ts +162 -5
- package/src/harness/llm-router.ts +3 -3
- package/src/harness/result-types.ts +14 -0
- package/src/harness/types-public.ts +9 -5
- package/src/index.ts +2 -1
- package/src/tools/get-primitives-ts.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-Ow-tNtFE.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.23.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": [
|
|
@@ -189,9 +189,9 @@
|
|
|
189
189
|
"puppeteer-core": "^24.40.0 || ^25.0.0",
|
|
190
190
|
"react": "^18.0.0 || ^19.0.0",
|
|
191
191
|
"react-dom": "^18.0.0 || ^19.0.0",
|
|
192
|
-
"@ggui-ai/
|
|
193
|
-
"@ggui-ai/
|
|
194
|
-
"@ggui-ai/project-config": "0.
|
|
192
|
+
"@ggui-ai/mcp-server-core": "0.23.0",
|
|
193
|
+
"@ggui-ai/protocol": "0.23.0",
|
|
194
|
+
"@ggui-ai/project-config": "0.23.0"
|
|
195
195
|
},
|
|
196
196
|
"peerDependenciesMeta": {
|
|
197
197
|
"@google/adk": {
|
|
@@ -233,11 +233,11 @@
|
|
|
233
233
|
"openai": "^7.9.0",
|
|
234
234
|
"typescript": "^5.0.0",
|
|
235
235
|
"zod": "^4.3.6",
|
|
236
|
-
"@ggui-ai/design": "0.
|
|
237
|
-
"@ggui-ai/gadgets": "0.
|
|
238
|
-
"@ggui-ai/sandbox": "0.
|
|
239
|
-
"@ggui-ai/
|
|
240
|
-
"@ggui-ai/
|
|
236
|
+
"@ggui-ai/design": "0.23.0",
|
|
237
|
+
"@ggui-ai/gadgets": "0.23.0",
|
|
238
|
+
"@ggui-ai/sandbox": "0.23.0",
|
|
239
|
+
"@ggui-ai/wire": "0.23.0",
|
|
240
|
+
"@ggui-ai/ui-visual-tester": "0.23.0"
|
|
241
241
|
},
|
|
242
242
|
"devDependencies": {
|
|
243
243
|
"@google/adk": "^2.0.0",
|
|
@@ -252,9 +252,9 @@
|
|
|
252
252
|
"react-dom": "^19.0.0",
|
|
253
253
|
"tsup": "^8.5.0",
|
|
254
254
|
"vitest": "^3.2.6",
|
|
255
|
-
"@ggui-ai/mcp-server-core": "0.
|
|
256
|
-
"@ggui-ai/project-config": "0.
|
|
257
|
-
"@ggui-ai/protocol": "0.
|
|
255
|
+
"@ggui-ai/mcp-server-core": "0.23.0",
|
|
256
|
+
"@ggui-ai/project-config": "0.23.0",
|
|
257
|
+
"@ggui-ai/protocol": "0.23.0"
|
|
258
258
|
},
|
|
259
259
|
"engines": {
|
|
260
260
|
"node": ">=22.0.0"
|
|
@@ -9,9 +9,12 @@
|
|
|
9
9
|
* 1. {@link CLAUDE_CODE_LOGIN_CREDENTIAL} — the sentinel a `ProviderKeyRef`
|
|
10
10
|
* carries in place of a key, the same shape `bedrock-iam` already uses
|
|
11
11
|
* for "authenticate with ambient credentials, not a key I hold".
|
|
12
|
-
* 2. {@link
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* 2. {@link loginChildEnv} — the spawned process sees an ALLOWLIST of the
|
|
13
|
+
* parent's env, never the parent's env minus a few names: no provider
|
|
14
|
+
* key (the binary prefers an env key over its login, so a stale
|
|
15
|
+
* `ANTHROPIC_API_KEY` would silently take the run off the login path),
|
|
16
|
+
* no other secret the parent merely holds, and no switch that would
|
|
17
|
+
* change what the binary does (ggui#1278).
|
|
15
18
|
* 3. {@link claudeCodeLoginQueryOptions} — the SDK options that keep the
|
|
16
19
|
* run bounded: no built-in tool (`tools: []`), no `~/.claude` settings,
|
|
17
20
|
* hooks or MCP servers leaking in (`settingSources: []`). Not
|
|
@@ -40,14 +43,66 @@ export const PROVIDER_KEY_ENV_NAMES = [
|
|
|
40
43
|
"ANTHROPIC_BASE_URL",
|
|
41
44
|
] as const;
|
|
42
45
|
|
|
43
|
-
/**
|
|
44
|
-
|
|
46
|
+
/**
|
|
47
|
+
* The only env names the login child inherits (ggui#1278), plus every
|
|
48
|
+
* `LC_*` locale name. The spawned binary sees a property of this code, not
|
|
49
|
+
* whatever the parent happened to load: a parent that sourced a whole
|
|
50
|
+
* `.env` holds every provider's key, and a denylist would pass all of them
|
|
51
|
+
* on, together with switches such as `CLAUDE_CODE_USE_BEDROCK` that would
|
|
52
|
+
* silently re-route the run.
|
|
53
|
+
*
|
|
54
|
+
* - Process basics: `PATH` (the binary runs helpers, e.g. the macOS
|
|
55
|
+
* keychain tool), `HOME`/`USER`/`LOGNAME` (where and whose the login
|
|
56
|
+
* store is), `SHELL`, temp dirs, `TERM`, `TZ`, locale.
|
|
57
|
+
* - Where the login lives: `CLAUDE_CONFIG_DIR`, the XDG base dirs.
|
|
58
|
+
* - `CLAUDE_CODE_OAUTH_TOKEN`: the login ITSELF when a headless host
|
|
59
|
+
* supplies it by env (`claude setup-token`). It is the one credential
|
|
60
|
+
* this path may use; every provider key stays out
|
|
61
|
+
* ({@link PROVIDER_KEY_ENV_NAMES} is never on this list).
|
|
62
|
+
* - Network reach: proxy variables in both spellings, and the extra-CA
|
|
63
|
+
* and certificate paths a corporate network needs.
|
|
64
|
+
*/
|
|
65
|
+
export const LOGIN_CHILD_ENV_NAMES = [
|
|
66
|
+
"PATH",
|
|
67
|
+
"HOME",
|
|
68
|
+
"USER",
|
|
69
|
+
"LOGNAME",
|
|
70
|
+
"SHELL",
|
|
71
|
+
"TMPDIR",
|
|
72
|
+
"TMP",
|
|
73
|
+
"TEMP",
|
|
74
|
+
"TERM",
|
|
75
|
+
"TZ",
|
|
76
|
+
"LANG",
|
|
77
|
+
"CLAUDE_CONFIG_DIR",
|
|
78
|
+
"CLAUDE_CODE_OAUTH_TOKEN",
|
|
79
|
+
"XDG_CONFIG_HOME",
|
|
80
|
+
"XDG_DATA_HOME",
|
|
81
|
+
"XDG_CACHE_HOME",
|
|
82
|
+
"XDG_STATE_HOME",
|
|
83
|
+
"XDG_RUNTIME_DIR",
|
|
84
|
+
"HTTPS_PROXY",
|
|
85
|
+
"https_proxy",
|
|
86
|
+
"HTTP_PROXY",
|
|
87
|
+
"http_proxy",
|
|
88
|
+
"NO_PROXY",
|
|
89
|
+
"no_proxy",
|
|
90
|
+
"ALL_PROXY",
|
|
91
|
+
"all_proxy",
|
|
92
|
+
"NODE_EXTRA_CA_CERTS",
|
|
93
|
+
"SSL_CERT_FILE",
|
|
94
|
+
"SSL_CERT_DIR",
|
|
95
|
+
] as const;
|
|
96
|
+
|
|
97
|
+
const LOGIN_CHILD_ENV: ReadonlySet<string> = new Set(LOGIN_CHILD_ENV_NAMES);
|
|
98
|
+
|
|
99
|
+
/** The env the login child is spawned with: the allowlisted names present in `env`, nothing else. */
|
|
100
|
+
export function loginChildEnv(
|
|
45
101
|
env: Readonly<Record<string, string | undefined>>
|
|
46
102
|
): Record<string, string> {
|
|
47
103
|
const out: Record<string, string> = {};
|
|
48
|
-
const drop: ReadonlySet<string> = new Set(PROVIDER_KEY_ENV_NAMES);
|
|
49
104
|
for (const [k, v] of Object.entries(env)) {
|
|
50
|
-
if (v !== undefined &&
|
|
105
|
+
if (v !== undefined && (LOGIN_CHILD_ENV.has(k) || k.startsWith("LC_"))) out[k] = v;
|
|
51
106
|
}
|
|
52
107
|
return out;
|
|
53
108
|
}
|
|
@@ -13,7 +13,7 @@ import type { ClaudeSdkConfig, GenerateParams } from '../base';
|
|
|
13
13
|
import type { AdapterResult, ProviderName, AdapterMode } from '../types';
|
|
14
14
|
import { createArtifacts, processMessage } from './message-parser';
|
|
15
15
|
import { createToolMcpServer } from './tool-bridge';
|
|
16
|
-
import { claudeCodeLoginQueryOptions,
|
|
16
|
+
import { claudeCodeLoginQueryOptions, loginChildEnv } from './claude-code-login';
|
|
17
17
|
|
|
18
18
|
export class ClaudeSdkAdapter extends GeneratorAdapter {
|
|
19
19
|
readonly provider: ProviderName = 'claude';
|
|
@@ -41,11 +41,12 @@ export class ClaudeSdkAdapter extends GeneratorAdapter {
|
|
|
41
41
|
const { query } = await import('@anthropic-ai/claude-agent-sdk');
|
|
42
42
|
|
|
43
43
|
// Build environment from config or process.env
|
|
44
|
-
// ggui#1185 — on the login path
|
|
45
|
-
// it prefers an env key over its login, so a stale
|
|
46
|
-
// run off this path without a word
|
|
44
|
+
// ggui#1185/#1278 — on the login path the binary sees an allowlisted
|
|
45
|
+
// env: no provider key (it prefers an env key over its login, so a stale
|
|
46
|
+
// one would take the run off this path without a word), and nothing
|
|
47
|
+
// else the parent merely holds.
|
|
47
48
|
const login = cfg.claudeCodeLogin === true;
|
|
48
|
-
const env = login ?
|
|
49
|
+
const env = login ? loginChildEnv(buildEnv(cfg.env)) : buildEnv(cfg.env);
|
|
49
50
|
|
|
50
51
|
const artifacts = createArtifacts();
|
|
51
52
|
const onProgress = cfg.onProgress;
|
|
@@ -349,6 +349,18 @@ async function runFastStage(
|
|
|
349
349
|
...(input.contract ? { contract: input.contract } : {}),
|
|
350
350
|
};
|
|
351
351
|
const result = await runRenderCheck(checkInput);
|
|
352
|
+
if (result.incomplete !== undefined) {
|
|
353
|
+
// ggui#1299: the check ran out of wall-clock time, so there is no
|
|
354
|
+
// evidence either way — the same posture as an unverified check
|
|
355
|
+
// (non-failing, the unverified score). The slow stage still gates.
|
|
356
|
+
return {
|
|
357
|
+
stage: 'fast',
|
|
358
|
+
ok: true,
|
|
359
|
+
score: UNVERIFIED_FAST_SCORE,
|
|
360
|
+
diagnostics: [],
|
|
361
|
+
durationMs: Date.now() - start,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
352
364
|
const diagnostics: StageDiagnostic[] = buildFastStageComplaints(
|
|
353
365
|
result.issues as readonly RenderCheckIssue[],
|
|
354
366
|
);
|
|
@@ -405,11 +417,14 @@ async function runSlowStage(
|
|
|
405
417
|
* job is just to land above or below it. Per-issue gradations matter
|
|
406
418
|
* less than getting the pass/fail boundary right.
|
|
407
419
|
*/
|
|
420
|
+
/** Fast-stage score when nothing failed but something went unverified. */
|
|
421
|
+
const UNVERIFIED_FAST_SCORE = 0.85;
|
|
422
|
+
|
|
408
423
|
function scoreFromRenderCheck(issues: readonly RenderCheckIssue[]): number {
|
|
409
424
|
const failed = issues.filter((i) => i.outcome === 'failed').length;
|
|
410
425
|
const unverified = issues.filter((i) => i.outcome === 'unverified').length;
|
|
411
426
|
if (failed === 0 && unverified === 0) return 1.0;
|
|
412
|
-
if (failed === 0) return
|
|
427
|
+
if (failed === 0) return UNVERIFIED_FAST_SCORE;
|
|
413
428
|
if (failed === 1) return 0.4;
|
|
414
429
|
if (failed === 2) return 0.3;
|
|
415
430
|
return 0.2;
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
|
|
24
24
|
import type { DataContract, JsonObject } from '@ggui-ai/protocol';
|
|
25
25
|
import { DEFAULT_RUNTIME_RENDER_CHECK } from './harness/check/runtime-render/index.js';
|
|
26
|
-
import type { EvalIssue } from './evaluation/types-public.js';
|
|
26
|
+
import type { EvalIssue, RuntimeProbeStatus } from './evaluation/types-public.js';
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
29
|
* Caller-supplied blueprint payload. `contract` is `unknown` at the
|
|
@@ -74,6 +74,23 @@ export interface ValidateBlueprintInput {
|
|
|
74
74
|
* the absence cleanly (selfCheck skips prop-coverage warnings; runtime
|
|
75
75
|
* probe skip-warns).
|
|
76
76
|
*/
|
|
77
|
+
/**
|
|
78
|
+
* The warning code for a probe that did not produce evidence. Exhaustive over
|
|
79
|
+
* the did-not-run statuses, so a new status is a compile error here instead of
|
|
80
|
+
* silently reading as whichever arm a ternary falls through to (ggui#1299:
|
|
81
|
+
* `timed-out` would otherwise have been labelled `probe-not-applicable`).
|
|
82
|
+
*/
|
|
83
|
+
function probeDidNotRunCode(status: Exclude<RuntimeProbeStatus, 'ran'>): string {
|
|
84
|
+
switch (status) {
|
|
85
|
+
case 'infra-skipped':
|
|
86
|
+
return 'runtime:probe-infra-failure';
|
|
87
|
+
case 'not-applicable':
|
|
88
|
+
return 'runtime:probe-not-applicable';
|
|
89
|
+
case 'timed-out':
|
|
90
|
+
return 'runtime:probe-timeout';
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
77
94
|
function asContract(x: RawContract): DataContract | undefined {
|
|
78
95
|
if (typeof x !== 'object' || x === null) return undefined;
|
|
79
96
|
return x as DataContract;
|
|
@@ -247,17 +264,15 @@ async function runtimeTier(input: RuntimeTierInput): Promise<RuntimeResult> {
|
|
|
247
264
|
fixtureProps: asJsonObject(input.fixtureProps),
|
|
248
265
|
});
|
|
249
266
|
if (outcome.status !== 'ran') {
|
|
250
|
-
// Probe
|
|
267
|
+
// Probe produced no evidence — infra failure, nothing to probe, or it
|
|
268
|
+
// ran out of wall-clock time before finishing.
|
|
251
269
|
// Surface as a warning rather than a hard error so a flaky probe
|
|
252
270
|
// doesn't permanently block legitimate blueprints — Claude can
|
|
253
271
|
// retry, and host-level logs record the diagnostic for ops review.
|
|
254
272
|
warnings.push({
|
|
255
273
|
_kind: 'warning',
|
|
256
274
|
tier: 'runtime',
|
|
257
|
-
code:
|
|
258
|
-
outcome.status === 'infra-skipped'
|
|
259
|
-
? 'runtime:probe-infra-failure'
|
|
260
|
-
: 'runtime:probe-not-applicable',
|
|
275
|
+
code: probeDidNotRunCode(outcome.status),
|
|
261
276
|
message: `Runtime probe did not run (${outcome.status}): ${outcome.reason ?? 'no reason recorded'}`,
|
|
262
277
|
});
|
|
263
278
|
return { errors, warnings };
|
|
@@ -250,6 +250,7 @@ export const FRAME_SIZING = `## Sizing — the frame owns the height and the wid
|
|
|
250
250
|
The host sizes the iframe; the component never sizes itself to the viewport. NEVER write \`100vh\` / \`100dvh\` / \`100svh\` (or an \`h-screen\` / \`min-h-screen\` class) as a height on any element:
|
|
251
251
|
- An inline card takes its natural height. A content-sized frame is measured FROM the content, so a \`100vh\` root can never shrink and grows the frame on every re-measure.
|
|
252
252
|
- Under fullscreen the frame already stretches your root to its full height. Centre inside it with flex (\`display: flex; flex-direction: column; justify-content: center\`), never with a viewport height.
|
|
253
|
+
- Under fullscreen the frame also keeps a 16 px inset between your content and the panel's edge — leave it. A \`surface="hero"\` band that OPENS the card runs edge to edge on its own. Any other band that is MEANT to run edge to edge (a full-width strip of rows, a closing action bar) takes \`bleed\` (\`<Box surface="sunken" bleed>…</Box>\`); only such a band should, and \`bleed\` is ignored outside fullscreen.
|
|
253
254
|
- To fill a parent that has a height use \`height: 100%\` or \`flex: 1\`; a viewport unit is only ever a \`max-height\` cap on a scroll region.
|
|
254
255
|
|
|
255
256
|
The width works the same way, and the mistake is the mirror image: **your outermost element FILLS the frame; a width cap belongs INSIDE it, on the column that holds text.**
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
-
<
|
|
2
|
-
{/* implement your UI here
|
|
3
|
-
|
|
1
|
+
<Box padding="md">
|
|
2
|
+
{/* implement your UI here — an inline card: the chat bubble around it
|
|
3
|
+
provides the chrome, so add no shadow or outer radius of your own.
|
|
4
|
+
Compact and natural height; stack vertically. This root fills the
|
|
5
|
+
bubble — wrap PROSE in <Container maxWidth="sm"> to cap the reading
|
|
6
|
+
measure (inert at bubble width, composed on a wider frame). */}
|
|
7
|
+
</Box>
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
-
<
|
|
2
|
-
{/* implement your UI here
|
|
3
|
-
|
|
1
|
+
<Box padding="md">
|
|
2
|
+
{/* implement your UI here — an inline card: the chat bubble around it
|
|
3
|
+
provides the chrome, so add no shadow or outer radius of your own.
|
|
4
|
+
Compact and natural height; stack vertically. This root fills the
|
|
5
|
+
bubble — wrap PROSE in <Container maxWidth="sm"> to cap the reading
|
|
6
|
+
measure (inert at bubble width, composed on a wider frame). */}
|
|
7
|
+
</Box>
|
|
@@ -416,7 +416,12 @@ export async function executeTool(
|
|
|
416
416
|
changes: normalizedChanges,
|
|
417
417
|
});
|
|
418
418
|
if (!patchResult.ok) {
|
|
419
|
-
|
|
419
|
+
// ggui#1261: the engine refused the patch (overlap, line bounds, missing range) before
|
|
420
|
+
// anything applied. Say so in the log, and call it what it is — PATCH_INVALID, like every
|
|
421
|
+
// other structural refusal in this tool — so the turn is never counted as a self-check
|
|
422
|
+
// failure that did not run.
|
|
423
|
+
console.log(`[coding-agent] apply_changes: REJECTED | ${patchResult.error}`);
|
|
424
|
+
return { result: `PATCH_INVALID: ${patchResult.error}`, error: true };
|
|
420
425
|
}
|
|
421
426
|
const candidate = patchResult.sourceAfter ?? currentFile;
|
|
422
427
|
|
package/src/contract-context.ts
CHANGED
|
@@ -59,7 +59,7 @@ const SHELL_HINTS: Record<string, string> = {
|
|
|
59
59
|
- Padding: use var(--ggui-spacing-2) for inner elements, var(--ggui-spacing-4) for sections. Tight.
|
|
60
60
|
- Scrolling: parent scrolls (chat feed) — component should NOT have internal scroll
|
|
61
61
|
- Layout: single column, compact. No sidebar. Stack everything vertically.
|
|
62
|
-
- Width: width: 100% (fills
|
|
62
|
+
- Width: the root is width: 100% (fills the bubble) — never cap the ROOT. A prose column inside it carries the reading cap (<Container maxWidth="sm">), which is inert at bubble width.`,
|
|
63
63
|
|
|
64
64
|
fullscreen: `**Fullscreen Shell — Takes over entire viewport**
|
|
65
65
|
- Container: the whole viewport, edge to edge — the frame stretches your root to its full height (you never set a viewport height yourself)
|
|
@@ -56,6 +56,7 @@ import {
|
|
|
56
56
|
} from '@ggui-ai/mcp-server-core';
|
|
57
57
|
import { createGeneratorTools } from './adapters/index.js';
|
|
58
58
|
import { dispatchGeneration } from './adapters/generation-dispatch.js';
|
|
59
|
+
import { generatorBuild } from './generator-build.js';
|
|
59
60
|
import { hasProfile } from './boilerplate/styling-profile.js';
|
|
60
61
|
import { effortDials } from './effort.js';
|
|
61
62
|
import type { ProviderName } from './adapters/types.js';
|
|
@@ -68,6 +69,7 @@ import {
|
|
|
68
69
|
} from './contract-context.js';
|
|
69
70
|
import type { RenderingContext } from './contract-context.js';
|
|
70
71
|
import type { DesignMode } from './design-mode.js';
|
|
72
|
+
import { DEFAULT_DESIGN_MODE } from './design-mode.js';
|
|
71
73
|
import { resolveRoute, applyRouteToEnv } from './adapters/provider-router.js';
|
|
72
74
|
import type { QualityConfig } from './evaluation/types-public.js';
|
|
73
75
|
import type { EvaluationConfig } from './evaluation/types.js';
|
|
@@ -339,6 +341,7 @@ export function createUiGenerator(
|
|
|
339
341
|
// Ignored by the constrained prompt (its shell/screen
|
|
340
342
|
// descriptors stay byte-identical).
|
|
341
343
|
const canvas = rendering ? canvasForRenderingContext(rendering) : undefined;
|
|
344
|
+
const shellType = harnessShellForRendering(rendering);
|
|
342
345
|
const promptWithVariance = injectVariance(
|
|
343
346
|
promptWithRendering,
|
|
344
347
|
input.variance,
|
|
@@ -405,6 +408,8 @@ export function createUiGenerator(
|
|
|
405
408
|
...(onRetry !== undefined ? { onRetry } : {}),
|
|
406
409
|
...(designMode !== undefined ? { designMode } : {}),
|
|
407
410
|
...(canvas !== undefined ? { canvas } : {}),
|
|
411
|
+
// ggui#1279 — the shell the declared rendering composes for (chat only; see harnessShellForRendering).
|
|
412
|
+
...(shellType !== undefined ? { shellType } : {}),
|
|
408
413
|
...(input.profile !== undefined ? { profile: input.profile } : {}),
|
|
409
414
|
// Fixture props reach the in-loop probe + visual round only (never the prompt, never identity).
|
|
410
415
|
...(input.fixtureProps !== undefined ? { fixtureProps: input.fixtureProps } : {}),
|
|
@@ -421,6 +426,8 @@ export function createUiGenerator(
|
|
|
421
426
|
latencyMs: Date.now() - startedAt,
|
|
422
427
|
cacheHit: false,
|
|
423
428
|
attempts: result.turnsUsed,
|
|
429
|
+
// ggui#1280 — the build that made this generation (template keys, never per-request).
|
|
430
|
+
build: generatorBuild(designMode ?? DEFAULT_DESIGN_MODE),
|
|
424
431
|
// Prompt-cache counters are provider-specific (Claude reports
|
|
425
432
|
// them; others omit). Pass through truthfully — absent on the
|
|
426
433
|
// adapter result stays absent here, never defaulted to 0.
|
|
@@ -461,6 +468,8 @@ export function createUiGenerator(
|
|
|
461
468
|
outputTokens: 0,
|
|
462
469
|
latencyMs: Date.now() - startedAt,
|
|
463
470
|
cacheHit: false,
|
|
471
|
+
// ggui#1280 — a failed generation still names the build that failed.
|
|
472
|
+
build: generatorBuild(designMode ?? DEFAULT_DESIGN_MODE),
|
|
464
473
|
},
|
|
465
474
|
};
|
|
466
475
|
} finally {
|
|
@@ -523,6 +532,27 @@ function mapLlmProviderToDispatchProvider(provider: LlmProvider): ProviderName {
|
|
|
523
532
|
}
|
|
524
533
|
}
|
|
525
534
|
|
|
535
|
+
/**
|
|
536
|
+
* ggui#1279 — the harness shell a declared rendering composes for. The canvas
|
|
537
|
+
* (`canvasForRenderingContext`) already follows the rendering; the WHAT leg's
|
|
538
|
+
* layout and the HOW leg's shell descriptor did not, so a declared chat card
|
|
539
|
+
* was scaffolded to "fill the frame" while the user prompt and the fit verdict
|
|
540
|
+
* treated it as an inline card. Only a declared CHAT shell changes anything.
|
|
541
|
+
* `fullscreen` IS the harness default, and `partial` has no layout of its own
|
|
542
|
+
* yet, so both pass no shell and those generations stay byte-identical — named
|
|
543
|
+
* here rather than guessed.
|
|
544
|
+
*/
|
|
545
|
+
function harnessShellForRendering(rendering: RenderingContext | undefined): 'chat' | undefined {
|
|
546
|
+
if (rendering === undefined) return undefined;
|
|
547
|
+
switch (rendering.shell) {
|
|
548
|
+
case 'chat':
|
|
549
|
+
return 'chat';
|
|
550
|
+
case 'fullscreen':
|
|
551
|
+
case 'partial':
|
|
552
|
+
return undefined;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
526
556
|
function mapRendering(
|
|
527
557
|
rendering: UiGenerateInput['rendering'],
|
|
528
558
|
): RenderingContext | undefined {
|
|
@@ -79,16 +79,41 @@ function runStateCoversPayload(input: AxisCheckInput): EvalIssue[] {
|
|
|
79
79
|
return issues;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Whether a `useState` initializer reads `props.<name>`: directly, or through a local bound from
|
|
84
|
+
* it (`const initial = normalize(props.initialProfile)` then `useState(initial.name)`, or a
|
|
85
|
+
* destructure `const { name } = props.initialProfile ?? {}` then `useState(name)`). The property
|
|
86
|
+
* this check guards is "form state is seeded from the prop", not a spelling: ggui#1261's Exp 011
|
|
87
|
+
* cell re-seeded correctly through a normalized local, was refused, and spent a turn restoring
|
|
88
|
+
* the literal form.
|
|
89
|
+
*/
|
|
90
|
+
function seedsFromProp(src: string, name: string): boolean {
|
|
91
|
+
const propRef = `props\\??\\.${name}\\b`;
|
|
92
|
+
const initializerReads = (ref: string): boolean =>
|
|
93
|
+
new RegExp(`useState(?:<[^>]*>)?\\s*\\([\\s\\S]{0,400}?${ref}`).test(src);
|
|
94
|
+
if (initializerReads(propRef)) return true;
|
|
95
|
+
const locals = new Set<string>();
|
|
96
|
+
for (const m of src.matchAll(new RegExp(`(?:const|let|var)\\s+(\\w+)\\s*(?::[^=]{1,80})?=\\s*[^;]{0,300}?${propRef}`, "g"))) {
|
|
97
|
+
const id = m[1];
|
|
98
|
+
if (id !== undefined) locals.add(id);
|
|
99
|
+
}
|
|
100
|
+
for (const m of src.matchAll(new RegExp(`(?:const|let|var)\\s*\\{([^}]{1,400})\\}\\s*=\\s*[^;]{0,300}?${propRef}`, "g"))) {
|
|
101
|
+
for (const part of (m[1] ?? "").split(",")) {
|
|
102
|
+
const id = part.split(":").pop()?.split("=")[0]?.trim();
|
|
103
|
+
if (id !== undefined && /^\w+$/.test(id)) locals.add(id);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
for (const id of locals) if (initializerReads(`\\b${id}\\b`)) return true;
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
|
|
82
110
|
function runInitialValuesSeeded(input: AxisCheckInput): EvalIssue[] {
|
|
83
111
|
if (input.compiledCode === null) return [];
|
|
84
112
|
const src = input.sourceCode;
|
|
85
113
|
const initialProps = getInitialValuePropNames(input.contract);
|
|
86
114
|
const issues: EvalIssue[] = [];
|
|
87
115
|
for (const name of initialProps) {
|
|
88
|
-
|
|
89
|
-
`useState(?:<[^>]*>)?\\s*\\([\\s\\S]{0,400}?props\\.${name}\\b`,
|
|
90
|
-
);
|
|
91
|
-
if (re.test(src)) continue;
|
|
116
|
+
if (seedsFromProp(src, name)) continue;
|
|
92
117
|
issues.push(
|
|
93
118
|
mkIssue(
|
|
94
119
|
"state.payload.initial_values_seeded",
|
package/src/evaluation/index.ts
CHANGED
|
@@ -25,7 +25,7 @@ import type {
|
|
|
25
25
|
} from './types-public.js';
|
|
26
26
|
import { LLM_EVAL_STATIC_CRITERIA } from './types-public.js';
|
|
27
27
|
import { createAgent } from '../harness/llm-router';
|
|
28
|
-
import type { AgentConfig, LLMToolDef, LLMAgent } from '../harness/llm-router';
|
|
28
|
+
import type { AgentConfig, LLMToolDef, LLMAgent, LLMToolCallResponse } from '../harness/llm-router';
|
|
29
29
|
import type { JsonObject } from '@ggui-ai/protocol';
|
|
30
30
|
import { getCriterionById } from './types-public.js';
|
|
31
31
|
import { DEFAULT_DESIGN_MODE, type DesignMode } from '../design-mode.js';
|
|
@@ -596,10 +596,36 @@ interface CriterionResult {
|
|
|
596
596
|
coverage: CriterionRunStatus;
|
|
597
597
|
/** Populated for `skipped` — why the criterion produced no verdict. */
|
|
598
598
|
skipReason?: string;
|
|
599
|
-
|
|
599
|
+
/** This call's token accounting. The cache counts are present only when the
|
|
600
|
+
* provider reported them — absent means unreported, never zero (ggui#1281). */
|
|
601
|
+
tokens: { in: number; out: number; cacheRead?: number; cacheCreation?: number };
|
|
600
602
|
durationMs: number;
|
|
601
603
|
}
|
|
602
604
|
|
|
605
|
+
/** A call's token accounting, carrying the prompt-cache counts only when the
|
|
606
|
+
* provider reported them (the router's rule: absent is "unreported"). */
|
|
607
|
+
function callTokens(response: LLMToolCallResponse): CriterionResult['tokens'] {
|
|
608
|
+
return {
|
|
609
|
+
in: response.inputTokens,
|
|
610
|
+
out: response.outputTokens,
|
|
611
|
+
...(response.cacheReadTokens !== undefined ? { cacheRead: response.cacheReadTokens } : {}),
|
|
612
|
+
...(response.cacheCreationTokens !== undefined ? { cacheCreation: response.cacheCreationTokens } : {}),
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* The evaluation's summed token accounting. `inputTokens` is the NON-cached
|
|
618
|
+
* input (the router's convention); the cache counts are the sums of what the
|
|
619
|
+
* calls reported, and stay absent when no call reported one — so a caller can
|
|
620
|
+
* tell "no cache reads" from "the provider does not say" (ggui#1281).
|
|
621
|
+
*/
|
|
622
|
+
export interface LLMEvalTokenUsage {
|
|
623
|
+
inputTokens: number;
|
|
624
|
+
outputTokens: number;
|
|
625
|
+
cacheReadTokens?: number;
|
|
626
|
+
cacheCreationTokens?: number;
|
|
627
|
+
}
|
|
628
|
+
|
|
603
629
|
/**
|
|
604
630
|
* All valid categories for LLM evaluation — derived from the ONE static
|
|
605
631
|
* list in types-public so the coverage stamp here and the harness's
|
|
@@ -642,7 +668,7 @@ async function evalCriterion(
|
|
|
642
668
|
pass: true,
|
|
643
669
|
coverage: 'skipped',
|
|
644
670
|
skipReason: 'no tool call returned',
|
|
645
|
-
tokens:
|
|
671
|
+
tokens: callTokens(response),
|
|
646
672
|
durationMs,
|
|
647
673
|
};
|
|
648
674
|
}
|
|
@@ -700,7 +726,7 @@ async function evalCriterion(
|
|
|
700
726
|
issues,
|
|
701
727
|
pass,
|
|
702
728
|
coverage: 'ran',
|
|
703
|
-
tokens:
|
|
729
|
+
tokens: callTokens(response),
|
|
704
730
|
durationMs,
|
|
705
731
|
};
|
|
706
732
|
} catch (e) {
|
|
@@ -833,7 +859,7 @@ export async function runLLMEvaluation(
|
|
|
833
859
|
context: LLMEvalContext,
|
|
834
860
|
config: LLMEvalConfig,
|
|
835
861
|
preWarmedContext?: PreWarmedEvalContext | null,
|
|
836
|
-
): Promise<EvalResult &
|
|
862
|
+
): Promise<EvalResult & LLMEvalTokenUsage> {
|
|
837
863
|
const providerName = config.provider;
|
|
838
864
|
const model = config.model || getDefaultEvalModel(providerName);
|
|
839
865
|
const routerProvider =
|
|
@@ -887,7 +913,10 @@ export async function runLLMEvaluation(
|
|
|
887
913
|
const passCategories: string[] = [];
|
|
888
914
|
let totalIn = 0;
|
|
889
915
|
let totalOut = 0;
|
|
890
|
-
|
|
916
|
+
// Measured, not estimated: summed from what the calls reported; absent
|
|
917
|
+
// while no call has reported one (ggui#1281).
|
|
918
|
+
let cacheRead: number | undefined;
|
|
919
|
+
let cacheCreation: number | undefined;
|
|
891
920
|
|
|
892
921
|
for (const r of results) {
|
|
893
922
|
allIssues.push(...r.issues);
|
|
@@ -896,12 +925,8 @@ export async function runLLMEvaluation(
|
|
|
896
925
|
}
|
|
897
926
|
totalIn += r.tokens.in;
|
|
898
927
|
totalOut += r.tokens.out;
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
// Estimate cached tokens (mother prompt sent N times, cached N-1 times)
|
|
902
|
-
const firstResult = results[0];
|
|
903
|
-
if (firstResult && firstResult.tokens.in > 0) {
|
|
904
|
-
cachedTokensEstimate = firstResult.tokens.in * (totalCriteriaCount - 1);
|
|
928
|
+
if (r.tokens.cacheRead !== undefined) cacheRead = (cacheRead ?? 0) + r.tokens.cacheRead;
|
|
929
|
+
if (r.tokens.cacheCreation !== undefined) cacheCreation = (cacheCreation ?? 0) + r.tokens.cacheCreation;
|
|
905
930
|
}
|
|
906
931
|
|
|
907
932
|
const wallMs = Date.now() - evalStart;
|
|
@@ -928,7 +953,7 @@ export async function runLLMEvaluation(
|
|
|
928
953
|
}
|
|
929
954
|
console.log(
|
|
930
955
|
`[eval] total: ${(wallMs / 1000).toFixed(1)}s wall (${totalCriteriaCount} parallel) | ` +
|
|
931
|
-
`in=${totalIn} out=${totalOut}
|
|
956
|
+
`in=${totalIn} out=${totalOut} cacheRead=${cacheRead ?? 'unreported'} cacheWrite=${cacheCreation ?? 'unreported'} | ` +
|
|
932
957
|
`issues=${allIssues.length} pass=${passCategories.length}` +
|
|
933
958
|
(skippedCount > 0 ? ` skipped=${skippedCount}` : ''),
|
|
934
959
|
);
|
|
@@ -939,5 +964,7 @@ export async function runLLMEvaluation(
|
|
|
939
964
|
criteriaCoverage,
|
|
940
965
|
inputTokens: totalIn,
|
|
941
966
|
outputTokens: totalOut,
|
|
967
|
+
...(cacheRead !== undefined ? { cacheReadTokens: cacheRead } : {}),
|
|
968
|
+
...(cacheCreation !== undefined ? { cacheCreationTokens: cacheCreation } : {}),
|
|
942
969
|
};
|
|
943
970
|
}
|
|
@@ -74,8 +74,28 @@ export interface EvalIssue {
|
|
|
74
74
|
* consumers MUST NOT count it as a pass.
|
|
75
75
|
* - `not-applicable`— nothing to probe (no compiled code, no contract
|
|
76
76
|
* surface, or no runtimeRender check configured).
|
|
77
|
+
* - `timed-out` — probe started but did not finish inside its
|
|
78
|
+
* wall-clock bound (ggui#1299). Carries ZERO
|
|
79
|
+
* evidence about the component: on a contended
|
|
80
|
+
* host an ordinary component crosses the bound.
|
|
81
|
+
* Consumers MUST NOT count it as a pass, and MUST
|
|
82
|
+
* NOT count it as a crash either. `elapsedMs` and
|
|
83
|
+
* `hostLoad` say how long it ran and how busy the
|
|
84
|
+
* host was, so a reader can tell the two cases apart.
|
|
77
85
|
*/
|
|
78
|
-
export type RuntimeProbeStatus = "ran" | "infra-skipped" | "not-applicable";
|
|
86
|
+
export type RuntimeProbeStatus = "ran" | "infra-skipped" | "not-applicable" | "timed-out";
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The host's 1-minute load average at a probe's start and end, beside the
|
|
90
|
+
* number of CPUs the process can use (ggui#1299). A load well above
|
|
91
|
+
* `cores` means the probe competed for CPU; a wall-clock bound crossed
|
|
92
|
+
* there says more about the host than about the component.
|
|
93
|
+
*/
|
|
94
|
+
export interface ProbeHostLoad {
|
|
95
|
+
readonly start: number;
|
|
96
|
+
readonly end: number;
|
|
97
|
+
readonly cores: number;
|
|
98
|
+
}
|
|
79
99
|
|
|
80
100
|
/**
|
|
81
101
|
* Probe execution meta stamped onto an `EvalResult` at the exit-decision
|
|
@@ -85,8 +105,12 @@ export type RuntimeProbeStatus = "ran" | "infra-skipped" | "not-applicable";
|
|
|
85
105
|
*/
|
|
86
106
|
export interface RuntimeProbeMeta {
|
|
87
107
|
readonly status: RuntimeProbeStatus;
|
|
88
|
-
/** Populated for `infra-skipped` / `not-applicable` — why the probe didn't
|
|
108
|
+
/** Populated for `infra-skipped` / `not-applicable` / `timed-out` — why the probe didn't finish. */
|
|
89
109
|
readonly reason?: string;
|
|
110
|
+
/** `timed-out` only: how long the probe ran before it was stopped. */
|
|
111
|
+
readonly elapsedMs?: number;
|
|
112
|
+
/** Host load around the probe, when it ran isolated (absent for an in-process probe). */
|
|
113
|
+
readonly hostLoad?: ProbeHostLoad;
|
|
90
114
|
}
|
|
91
115
|
|
|
92
116
|
/**
|
|
@@ -175,6 +199,14 @@ export function notApplicableCoverage(reason: string): CriterionCoverage[] {
|
|
|
175
199
|
}));
|
|
176
200
|
}
|
|
177
201
|
|
|
202
|
+
/** One contract-feedback round (ggui#1261): what bought it, and the source it was fed back on. */
|
|
203
|
+
export interface ContractFeedbackRecord {
|
|
204
|
+
/** The exit-probe issue subcategories that bought the round, e.g. `runtime:prop-sensitivity:currentUser`. */
|
|
205
|
+
readonly firedOn: readonly string[];
|
|
206
|
+
/** The component source as it stood when the model was told — the round's BEFORE. */
|
|
207
|
+
readonly sourceBefore: string;
|
|
208
|
+
}
|
|
209
|
+
|
|
178
210
|
export interface EvalResult {
|
|
179
211
|
issues: EvalIssue[];
|
|
180
212
|
pass: string[];
|
|
@@ -194,6 +226,14 @@ export interface EvalResult {
|
|
|
194
226
|
* probe runner; absent on eval paths that never invoke the probe.
|
|
195
227
|
*/
|
|
196
228
|
runtimeProbe?: RuntimeProbeMeta;
|
|
229
|
+
/**
|
|
230
|
+
* The contract-feedback round, when one fired this generation (ggui#1261):
|
|
231
|
+
* which exit-probe findings bought it and the source as it stood when the
|
|
232
|
+
* model was told. `runtimeProbe` and the `runtime:*` issues on this result
|
|
233
|
+
* are the probe's verdict AFTER that round; `sourceBefore` beside the final
|
|
234
|
+
* source is the round's before/after. Absent when no such round fired.
|
|
235
|
+
*/
|
|
236
|
+
contractFeedback?: ContractFeedbackRecord;
|
|
197
237
|
/**
|
|
198
238
|
* Per-canvas visual verdicts. Stamped by the harness ONLY when the
|
|
199
239
|
* visual leg ran with `visualEvaluation.canvases` set (arm-neutral —
|