@ggui-ai/ui-gen 0.20.0 → 0.21.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.
Files changed (80) hide show
  1. package/dist/adapters/base.d.ts +7 -0
  2. package/dist/adapters/base.js +1 -0
  3. package/dist/adapters/base.js.map +1 -1
  4. package/dist/adapters/generation-dispatch.d.ts +17 -3
  5. package/dist/adapters/generation-dispatch.js +959 -3484
  6. package/dist/adapters/generation-dispatch.js.map +1 -1
  7. package/dist/adapters/index.js +35 -3
  8. package/dist/adapters/index.js.map +1 -1
  9. package/dist/advanced/index.d.ts +4 -4
  10. package/dist/advanced/index.js +1039 -3573
  11. package/dist/advanced/index.js.map +1 -1
  12. package/dist/blueprint-validator.js +10 -3
  13. package/dist/blueprint-validator.js.map +1 -1
  14. package/dist/boilerplate.js +2 -2
  15. package/dist/boilerplate.js.map +1 -1
  16. package/dist/check/index.d.ts +1 -1
  17. package/dist/check/index.js.map +1 -1
  18. package/dist/{create-ui-generator-CxPvudbs.d.ts → create-ui-generator-D7kOXNjJ.d.ts} +2 -2
  19. package/dist/{dispatch-N8wdNOX-.d.ts → dispatch-DGvhmARp.d.ts} +1 -1
  20. package/dist/evaluation/axis-checks/index.d.ts +3 -3
  21. package/dist/evaluation/axis-checks/index.js.map +1 -1
  22. package/dist/evaluation/axis-checks/registry.d.ts +1 -1
  23. package/dist/evaluation/index.d.ts +38 -6
  24. package/dist/evaluation/index.js +649 -3370
  25. package/dist/evaluation/index.js.map +1 -1
  26. package/dist/harness/check/runtime-render/index.d.ts +2 -2
  27. package/dist/harness/check/runtime-render/index.js +10 -3
  28. package/dist/harness/check/runtime-render/index.js.map +1 -1
  29. package/dist/harness/check/runtime-render/render-check-worker.js +5 -3
  30. package/dist/harness/check/runtime-render/render-check-worker.js.map +1 -1
  31. package/dist/harness/index.d.ts +2 -2
  32. package/dist/harness/index.js +562 -30
  33. package/dist/harness/index.js.map +1 -1
  34. package/dist/harness/llm-trace-sink.d.ts +7 -1
  35. package/dist/harness/llm-trace-sink.js.map +1 -1
  36. package/dist/harness/result-types.d.ts +1 -1
  37. package/dist/harness/runtime.d.ts +11 -2
  38. package/dist/harness/runtime.js +3 -3
  39. package/dist/harness/runtime.js.map +1 -1
  40. package/dist/harness/types-public.d.ts +1 -1
  41. package/dist/harness/types-public.js +2 -2
  42. package/dist/harness/types-public.js.map +1 -1
  43. package/dist/harness/validator-trace-sink.d.ts +1 -1
  44. package/dist/index.d.ts +3 -3
  45. package/dist/index.js +1039 -3573
  46. package/dist/index.js.map +1 -1
  47. package/dist/{llm-router-D7fjBhtr.d.ts → llm-router-DwwUK1bz.d.ts} +16 -3
  48. package/dist/patch.d.ts +1 -1
  49. package/dist/providers/index.js +5 -4
  50. package/dist/providers/index.js.map +1 -1
  51. package/dist/{render-check-WfOVO9kU.d.ts → render-check-C0CqVceF.d.ts} +17 -0
  52. package/dist/{types-public-DKR_h70H.d.ts → types-public-CzYqZclY.d.ts} +44 -1
  53. package/dist/validation/index.d.ts +1 -1
  54. package/dist/validation/index.js +1 -1
  55. package/dist/validation/index.js.map +1 -1
  56. package/dist/workflows.d.ts +1 -1
  57. package/package.json +16 -12
  58. package/src/adapters/base.ts +8 -0
  59. package/src/adapters/claude/claude-code-login.ts +73 -0
  60. package/src/adapters/claude/sdk.ts +8 -1
  61. package/src/adapters/generation-dispatch.ts +19 -2
  62. package/src/adapters/openai/tokens.ts +18 -0
  63. package/src/adapters/provider-router.ts +28 -0
  64. package/src/boilerplate/generate.ts +19 -4
  65. package/src/boilerplate/hard-sections.ts +1 -1
  66. package/src/create-ui-generator.ts +19 -10
  67. package/src/evaluation/design-identity.ts +16 -5
  68. package/src/evaluation/types-public.ts +42 -0
  69. package/src/evaluation/visual-evaluator.ts +231 -24
  70. package/src/harness/check/runtime-render/adapter.ts +9 -0
  71. package/src/harness/check/runtime-render/render-check.ts +26 -5
  72. package/src/harness/claude-code-login-query.ts +528 -0
  73. package/src/harness/coding/assemble-result.ts +10 -1
  74. package/src/harness/coding/run-eval-round.ts +179 -34
  75. package/src/harness/enforced-coding.ts +20 -0
  76. package/src/harness/llm-router.ts +390 -55
  77. package/src/harness/llm-trace-sink.ts +7 -1
  78. package/src/harness/runtime.ts +6 -0
  79. package/src/providers/openai.ts +31 -19
  80. package/src/validation/primitives.ts +1 -1
@@ -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-DKR_h70H.js';
5
+ import './types-public-CzYqZclY.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.20.0",
3
+ "version": "0.21.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": [
@@ -185,12 +185,13 @@
185
185
  "peerDependencies": {
186
186
  "@google/adk": "^2.0.0",
187
187
  "@openai/agents": "^0.17.0",
188
+ "@sparticuz/chromium": "^149.0.0",
188
189
  "puppeteer-core": "^24.40.0 || ^25.0.0",
189
190
  "react": "^18.0.0 || ^19.0.0",
190
191
  "react-dom": "^18.0.0 || ^19.0.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"
192
+ "@ggui-ai/protocol": "0.21.0",
193
+ "@ggui-ai/mcp-server-core": "0.21.0",
194
+ "@ggui-ai/project-config": "0.21.0"
194
195
  },
195
196
  "peerDependenciesMeta": {
196
197
  "@google/adk": {
@@ -199,6 +200,9 @@
199
200
  "@openai/agents": {
200
201
  "optional": true
201
202
  },
203
+ "@sparticuz/chromium": {
204
+ "optional": true
205
+ },
202
206
  "puppeteer-core": {
203
207
  "optional": true
204
208
  },
@@ -229,11 +233,11 @@
229
233
  "openai": "^7.9.0",
230
234
  "typescript": "^5.0.0",
231
235
  "zod": "^4.3.6",
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"
236
+ "@ggui-ai/design": "0.21.0",
237
+ "@ggui-ai/gadgets": "0.21.0",
238
+ "@ggui-ai/sandbox": "0.21.0",
239
+ "@ggui-ai/ui-visual-tester": "0.21.0",
240
+ "@ggui-ai/wire": "0.21.0"
237
241
  },
238
242
  "devDependencies": {
239
243
  "@google/adk": "^2.0.0",
@@ -248,9 +252,9 @@
248
252
  "react-dom": "^19.0.0",
249
253
  "tsup": "^8.5.0",
250
254
  "vitest": "^3.2.6",
251
- "@ggui-ai/project-config": "0.20.0",
252
- "@ggui-ai/protocol": "0.20.0",
253
- "@ggui-ai/mcp-server-core": "0.20.0"
255
+ "@ggui-ai/mcp-server-core": "0.21.0",
256
+ "@ggui-ai/project-config": "0.21.0",
257
+ "@ggui-ai/protocol": "0.21.0"
254
258
  },
255
259
  "engines": {
256
260
  "node": ">=22.0.0"
@@ -14,6 +14,13 @@ export interface AdapterConfig {
14
14
  apiKey?: string;
15
15
  /** Use Bedrock instead of direct API (Claude only) */
16
16
  useBedrock?: boolean;
17
+ /**
18
+ * ggui#1185 — run on the machine's own Claude Code login instead of a key.
19
+ * Claude SDK adapter only. The adapter strips every provider key from the
20
+ * spawned env and pins the run tool-less, config-less and non-bare (see
21
+ * `claude/claude-code-login.ts`). Opt-in; absent ⇒ key-based auth.
22
+ */
23
+ claudeCodeLogin?: boolean;
17
24
  /**
18
25
  * Thinking level for models with extended thinking (Gemini 3.x Flash
19
26
  * via the Interactions API). Mirrors the API's `thinking_level` enum —
@@ -129,5 +136,6 @@ export function hasCredentials(
129
136
  ): boolean {
130
137
  if (config.apiKey) return true;
131
138
  if (config.useBedrock) return true;
139
+ if (config.claudeCodeLogin) return true;
132
140
  return envVarNames.some((name) => !!process.env[name]);
133
141
  }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * ggui#1185 — generation through the machine's own Claude Code login.
3
+ *
4
+ * The Claude SDK adapter spawns the Claude Code binary. Given no provider
5
+ * key, that binary authenticates with whatever `claude` is logged in as on
6
+ * the machine — the operator's own plan. This module holds the three
7
+ * things that make such a run honest and reproducible:
8
+ *
9
+ * 1. {@link CLAUDE_CODE_LOGIN_CREDENTIAL} — the sentinel a `ProviderKeyRef`
10
+ * carries in place of a key, the same shape `bedrock-iam` already uses
11
+ * for "authenticate with ambient credentials, not a key I hold".
12
+ * 2. {@link stripProviderKeyEnv} — no provider key may reach the spawned
13
+ * process: the binary prefers an env key over its login, so a stale
14
+ * `ANTHROPIC_API_KEY` would silently take the run off the login path.
15
+ * 3. {@link claudeCodeLoginQueryOptions} — the SDK options that keep the
16
+ * run bounded: no built-in tool (`tools: []`), no `~/.claude` settings,
17
+ * hooks or MCP servers leaking in (`settingSources: []`). Not
18
+ * `--bare`, either: on the SDK's bundled binary `-p` reads the login
19
+ * store as it always has, so no flag is needed — and the flag it
20
+ * would be (`--no-bare`) is one that binary rejects; see the note on
21
+ * the function below.
22
+ *
23
+ * Opt-in, off by default; the default stays a provider key. The copy that
24
+ * names what this does lives with the CLI flag that enables it.
25
+ */
26
+
27
+ import type { Options } from "@anthropic-ai/claude-agent-sdk";
28
+
29
+ /** Sentinel credential: "no key — use the machine's Claude Code login". */
30
+ export const CLAUDE_CODE_LOGIN_CREDENTIAL = "claude-code-login" as const;
31
+
32
+ /**
33
+ * Every env name the spawned binary would read as a credential or an
34
+ * endpoint override ahead of its login. Stripped whole, never rewritten.
35
+ */
36
+ export const PROVIDER_KEY_ENV_NAMES = [
37
+ "ANTHROPIC_API_KEY",
38
+ "CLAUDE_API_KEY",
39
+ "ANTHROPIC_AUTH_TOKEN",
40
+ "ANTHROPIC_BASE_URL",
41
+ ] as const;
42
+
43
+ /** A copy of `env` without any provider key or endpoint override. */
44
+ export function stripProviderKeyEnv(
45
+ env: Readonly<Record<string, string | undefined>>
46
+ ): Record<string, string> {
47
+ const out: Record<string, string> = {};
48
+ const drop: ReadonlySet<string> = new Set(PROVIDER_KEY_ENV_NAMES);
49
+ for (const [k, v] of Object.entries(env)) {
50
+ if (v !== undefined && !drop.has(k)) out[k] = v;
51
+ }
52
+ return out;
53
+ }
54
+
55
+ /**
56
+ * The SDK `query()` options a login-path run is pinned to — typed as the
57
+ * SDK's own `Options` members, so a change in the SDK's shape is a compile
58
+ * error here and never a silently ignored key.
59
+ *
60
+ * NOT pinned: `--no-bare`. The SDK spawns its BUNDLED binary (2.1.229 for
61
+ * SDK 0.3.229), which has no bare mode and rejects the flag outright
62
+ * (`error: unknown option '--no-bare'`, measured 2026-09-21 on the first
63
+ * live turn of the login client); the machine's own `claude` (2.1.274)
64
+ * accepts it, which is where the earlier probe measured it. On the
65
+ * bundled binary `-p` reads the login store as it always has. If a later
66
+ * SDK bundles a binary where bare becomes the `-p` default, the flag
67
+ * comes back here — with the SDK version bump, measured again.
68
+ */
69
+ export type ClaudeCodeLoginQueryOptions = Required<Pick<Options, "tools" | "settingSources">>;
70
+
71
+ export function claudeCodeLoginQueryOptions(): ClaudeCodeLoginQueryOptions {
72
+ return { tools: [], settingSources: [] };
73
+ }
@@ -13,6 +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, stripProviderKeyEnv } from './claude-code-login';
16
17
 
17
18
  export class ClaudeSdkAdapter extends GeneratorAdapter {
18
19
  readonly provider: ProviderName = 'claude';
@@ -40,7 +41,11 @@ export class ClaudeSdkAdapter extends GeneratorAdapter {
40
41
  const { query } = await import('@anthropic-ai/claude-agent-sdk');
41
42
 
42
43
  // Build environment from config or process.env
43
- const env = buildEnv(cfg.env);
44
+ // ggui#1185 — on the login path no provider key may reach the binary:
45
+ // it prefers an env key over its login, so a stale one would take the
46
+ // run off this path without a word.
47
+ const login = cfg.claudeCodeLogin === true;
48
+ const env = login ? stripProviderKeyEnv(buildEnv(cfg.env)) : buildEnv(cfg.env);
44
49
 
45
50
  const artifacts = createArtifacts();
46
51
  const onProgress = cfg.onProgress;
@@ -72,6 +77,8 @@ export class ClaudeSdkAdapter extends GeneratorAdapter {
72
77
  allowDangerouslySkipPermissions: true,
73
78
  ...(cfg.cliPath && { pathToClaudeCodeExecutable: cfg.cliPath }),
74
79
  ...(cfg.stderr && { stderr: cfg.stderr }),
80
+ // ggui#1185 — tool-less, config-less, non-bare; the model is pinned above.
81
+ ...(login ? claudeCodeLoginQueryOptions() : {}),
75
82
  },
76
83
  })) {
77
84
  processMessage(
@@ -102,6 +102,12 @@ export interface GenerationDispatchParams {
102
102
  * today's single screenshot at `viewport`.
103
103
  */
104
104
  canvases?: readonly CanvasClass[];
105
+ /**
106
+ * ggui#1195 — per-canvas viewport override for the in-loop round: the
107
+ * box the composer targeted (`rendering.viewport`) so the round judges
108
+ * the same box. See `VisualEvalConfig.canvasViewports`.
109
+ */
110
+ canvasViewports?: Partial<Readonly<Record<CanvasClass, { width: number; height: number }>>>;
105
111
  };
106
112
  /** Quality config controlling eval tiers and improvement behavior */
107
113
  qualityConfig?: QualityConfig;
@@ -179,7 +185,12 @@ export interface GenerationDispatchParams {
179
185
  * means `constrained` (byte-identical to the pre-`designMode` path).
180
186
  */
181
187
  designMode?: DesignMode;
182
- /** Rendering canvas class for the `free` prompt — see `CreateHarnessInput.canvas`. */
188
+ /**
189
+ * Rendering canvas class — see `CreateHarnessInput.canvas`. Absent, the
190
+ * harness derives it from `shellType` × `screen` (ggui#1117), so the axis
191
+ * checks always judge for the canvas the generation renders on; the
192
+ * `free` prompt renders it, the constrained prompt ignores it.
193
+ */
183
194
  canvas?: CanvasClass;
184
195
  /** The app's generation profile (#991). */
185
196
  profile?: GenerationProfileInput;
@@ -296,7 +307,13 @@ export async function dispatchGeneration(
296
307
  : {}),
297
308
  // Design mode + canvas — the harness carries them to every leg.
298
309
  ...(params.designMode !== undefined ? { designMode: params.designMode } : {}),
299
- ...(params.canvas !== undefined ? { canvas: params.canvas } : {}),
310
+ // ggui#1117 — the canvas is ALWAYS carried: a stated one, else the class
311
+ // shell × screen renders on. The axis checks judge for it
312
+ // (`universal.root_width_cap` stands down without one — which is what every
313
+ // served generation got, since the serving runtime states no canvas); the
314
+ // constrained prompt ignores it (INVARIANT 1, `design-mode.pin.test.ts`),
315
+ // so the prompt digests do not move; the free prompt renders it as before.
316
+ canvas: params.canvas ?? canvasForRendering(params.shellType, params.screen),
300
317
  ...(params.profile !== undefined ? { profile: params.profile } : {}),
301
318
  // Policy is pre-resolved at the dispatch boundary so `createHarness`
302
319
  // stays free of experiment plumbing. `resolveHarnessPolicy` applies
@@ -31,6 +31,24 @@ export function splitOpenAiUsage(
31
31
  inputTokensInclCached: number,
32
32
  outputTokens: number,
33
33
  cachedTokens: number,
34
+ ): { tokens: AdapterResult['tokens']; cacheReadTokens: number } {
35
+ return splitCacheInclusiveUsage(inputTokensInclCached, outputTokens, cachedTokens);
36
+ }
37
+
38
+ /**
39
+ * The provider-neutral form of the split above (ggui#1186, third site): any
40
+ * provider whose reported input token count INCLUDES the cached part of the
41
+ * prompt — OpenAI's `input_tokens` + `input_tokens_details.cached_tokens`,
42
+ * Gemini's `total_input_tokens` + `total_cached_tokens` and
43
+ * `promptTokenCount` + `cachedContentTokenCount` ("the cached part of the
44
+ * prompt", per the SDK) — is split the same way into the non-cached input and
45
+ * the cached subset, so `input` is never double-counted and `total` stays the
46
+ * full footprint.
47
+ */
48
+ export function splitCacheInclusiveUsage(
49
+ inputTokensInclCached: number,
50
+ outputTokens: number,
51
+ cachedTokens: number,
34
52
  ): { tokens: AdapterResult['tokens']; cacheReadTokens: number } {
35
53
  // Never let a malformed usage report drive input negative.
36
54
  const cacheRead = Math.min(
@@ -12,6 +12,7 @@
12
12
  // legacy) still needs the wire-canonical anthropic id → cross-region
13
13
  // profile id upcast.
14
14
 
15
+ import { CLAUDE_CODE_LOGIN_CREDENTIAL, PROVIDER_KEY_ENV_NAMES } from './claude/claude-code-login';
15
16
  import type { LlmRoute } from '@ggui-ai/protocol';
16
17
 
17
18
  /**
@@ -58,6 +59,13 @@ export function getBedrockModelId(model: string): string {
58
59
  }
59
60
 
60
61
  export interface RoutingDecision {
62
+ /**
63
+ * Named only for the Claude Code login path (ggui#1185): the caller passed
64
+ * {@link CLAUDE_CODE_LOGIN_CREDENTIAL} as the key, every provider key is
65
+ * cleared from the env below, and the adapter pins the SDK options that keep
66
+ * the run tool-less, config-less and non-bare. Absent ⇒ key-based auth.
67
+ */
68
+ readonly auth?: 'claude-code-login';
61
69
  /**
62
70
  * Upstream model id the dispatch adapter will pass to the provider
63
71
  * SDK. For most routes this is `route.model` verbatim — the typed
@@ -143,6 +151,13 @@ function clearSiblings(
143
151
  */
144
152
  export function resolveRoute(input: RoutingInput): RoutingDecision {
145
153
  const { route, apiKey, env } = input;
154
+ if (apiKey === CLAUDE_CODE_LOGIN_CREDENTIAL && route.provider !== 'anthropic') {
155
+ throw new Error(
156
+ `${route.provider}:${route.model} cannot use the claude-code-login credential — ` +
157
+ `the machine's Claude Code login authenticates the Claude Code binary only; ` +
158
+ `pick an anthropic route or supply a ${route.provider} API key.`,
159
+ );
160
+ }
146
161
 
147
162
  switch (route.provider) {
148
163
  case 'openai': {
@@ -215,6 +230,19 @@ export function resolveRoute(input: RoutingInput): RoutingDecision {
215
230
  }
216
231
 
217
232
  case 'anthropic': {
233
+ if (apiKey === CLAUDE_CODE_LOGIN_CREDENTIAL) {
234
+ // ggui#1185: no key reaches the spawned binary — it authenticates with
235
+ // the machine's own Claude Code login. Every provider key AND every
236
+ // sibling is cleared, because the binary prefers an env key over its
237
+ // login and a stale one would silently take the run off this path.
238
+ const cleared: Record<string, undefined> = {};
239
+ for (const k of PROVIDER_KEY_ENV_NAMES) cleared[k] = undefined;
240
+ return {
241
+ model: route.model,
242
+ env: { ...cleared, ...clearSiblings(new Set()) },
243
+ auth: 'claude-code-login',
244
+ };
245
+ }
218
246
  if (apiKey) {
219
247
  return {
220
248
  model: route.model,
@@ -246,8 +246,20 @@ export function generateBoilerplate(
246
246
  }
247
247
 
248
248
  const toolNote = tool ? ` (label "${label}", nextStep hint → ${tool})` : "";
249
+ // ggui#1190: the control's copy is the contract's label, VERBATIM — stated for
250
+ // EVERY action (it used to appear only beside a `nextStep` hint), because the
251
+ // model given `"Confirm & Schedule"` in the contract block wrote
252
+ // `'Confirm Schedule'`. The rule lives in DATA_PARAMETERIZATION rule 4; this is
253
+ // its reminder beside the action, and the axis check
254
+ // `universal.action_label_dropped` names the drop when both are ignored.
255
+ // ggui#1244: it rides the payload type's doc comment, NOT the hook line. Placed
256
+ // on the hook line directly before the once-hint below, the two read as the
257
+ // control's spec and gemini-3.5-flash-lite guarded the repeatable Save 8/8;
258
+ // here, with the hook line back to its earlier bytes, 5/24 — within noise of
259
+ // the rate before either hint moved (1/8) — while one-time commitments stayed
260
+ // guarded 40/40 across three providers and every label stayed verbatim (Exp 009).
249
261
  actionTypeAliases.push(
250
- `/** Action payload: ${desc || label}${toolNote} */\ntype ${typeName} = ${tsType};`
262
+ `/** Action payload: ${desc || label}${toolNote} — control copy: "${label}" VERBATIM */\ntype ${typeName} = ${tsType};`
251
263
  );
252
264
  // Inline signature tells the LLM exactly how to call this action
253
265
  const callSig = tsType === "void" ? "() => void — fire and forget" : `(data: ${tsType}) => void`;
@@ -255,9 +267,12 @@ export function generateBoilerplate(
255
267
  // ggui#1108 (mitigation): the reminder sits at the EXACT site the model
256
268
  // writes the handler. It is a hint, not the rule — the rule is the prompt's
257
269
  // `TERMINAL_ACTIONS` section, because only the request and the contract say
258
- // which actions are meant once. A site comment alone moves generation by
259
- // roughly nothing (Exp 002b: adoption 1/36), which is why it is the
260
- // smallest of the three legs and never the one relied on.
270
+ // which actions are meant once. It is NOT inert, whatever Exp 002b measured
271
+ // for a different site comment (adoption 1/36): Exp 009 (ggui#1244) removed
272
+ // it and one-time scheduling went armed on all three providers (guarded 9/12
273
+ // against 24/24), and a second hint placed beside it turned it into an
274
+ // instruction for every action. So it stays, ALONE on the hook line — what
275
+ // sits next to it is part of what it says.
261
276
  const onceHint = " — if this is meant ONCE, disable its control after it fires";
262
277
  actionHookCalls.push(` const ${key} = useAction<${typeName}>('${key}'); // ${callSig}${toolHint}${onceHint}`);
263
278
  actionReturnFields.push(key);
@@ -213,7 +213,7 @@ Rules:
213
213
  1. All request-specific data → props with defaults. City names, tickers, user names, dates, counts.
214
214
  2. Layout and styling are universal. Colors, spacing, structure — these are the reusable part.
215
215
  3. Default values come from the current request — so the component renders correctly standalone.
216
- 4. Copy comes from the props. NEVER invent display text the contract does not carry — no eyebrow / kicker / overline (a \`<Text caps>\` label), no helper line, no status text, no tagline. **Do NOT add a caps label above the heading** — not \`<Text caps>LET'S BEGIN</Text>\`, not \`WELCOME\`, not a section kicker like \`GETTING STARTED\` or \`OVERVIEW\` — UNLESS a prop supplies that exact text (\`<Text caps>{props.section}</Text>\`). When the request asks for a heading and a line, render the heading and the line and NOTHING above the heading. A heading's fallback is the request's own words, never a slogan of your own. Text the agent cannot change is chatter on every render, and a caps eyebrow you wrote is the most common form of it.
216
+ 4. Copy comes from the props. NEVER invent display text the contract does not carry — no eyebrow / kicker / overline (a \`<Text caps>\` label), no helper line, no status text, no tagline. **Do NOT add a caps label above the heading** — not \`<Text caps>LET'S BEGIN</Text>\`, not \`WELCOME\`, not a section kicker like \`GETTING STARTED\` or \`OVERVIEW\` — UNLESS a prop supplies that exact text (\`<Text caps>{props.section}</Text>\`). When the request asks for a heading and a line, render the heading and the line and NOTHING above the heading. A heading's fallback is the request's own words, never a slogan of your own. Text the agent cannot change is chatter on every render, and a caps eyebrow you wrote is the most common form of it. An action's control shows the contract's \`label\` VERBATIM — every character, including \`&\`, \`/\`, \`–\` and quotes — never paraphrased, shortened or re-cased (\`"Confirm & Schedule"\` is not \`"Confirm Schedule"\`).
217
217
  5. Props interface must be typed and exported.`;
218
218
 
219
219
  /**
@@ -298,16 +298,25 @@ export function createUiGenerator(
298
298
  // structurally into the dispatch call instead. `route.env`
299
299
  // already carries exactly the key(s) that would have gone into
300
300
  // `process.env`, so no extra resolution work is needed here.
301
- const routeOverride = disableEnvMutation
302
- ? {
303
- apiKey:
304
- route.env['ANTHROPIC_API_KEY'] ??
305
- route.env['OPENAI_API_KEY'] ??
306
- route.env['GEMINI_API_KEY'] ??
307
- route.env['OPENROUTER_API_KEY'],
308
- useBedrock: route.env['CLAUDE_CODE_USE_BEDROCK'] === '1',
309
- }
310
- : undefined;
301
+ //
302
+ // ggui#1185 — the login path has NO key to write anywhere: the
303
+ // route decision `auth: 'claude-code-login'` is carried as
304
+ // `routeOverride.claudeCodeLogin` in BOTH env modes, because the
305
+ // flag is the only way the decision reaches `createAgent` /
306
+ // `createVisionAgent` (which select the login client on it).
307
+ const routeOverride =
308
+ route.auth === 'claude-code-login'
309
+ ? { claudeCodeLogin: true }
310
+ : disableEnvMutation
311
+ ? {
312
+ apiKey:
313
+ route.env['ANTHROPIC_API_KEY'] ??
314
+ route.env['OPENAI_API_KEY'] ??
315
+ route.env['GEMINI_API_KEY'] ??
316
+ route.env['OPENROUTER_API_KEY'],
317
+ useBedrock: route.env['CLAUDE_CODE_USE_BEDROCK'] === '1',
318
+ }
319
+ : undefined;
311
320
 
312
321
  try {
313
322
  const tools = createGeneratorTools({
@@ -14,7 +14,7 @@
14
14
  // that carries the same rendering inputs.
15
15
  import { createHash } from 'node:crypto';
16
16
  import { readFileSync, readdirSync, statSync } from 'node:fs';
17
- import { resolve } from 'node:path';
17
+ import { relative, resolve } from 'node:path';
18
18
 
19
19
  export interface JudgeDesignIdentity {
20
20
  /** Absolute path of the design `src` tree the judge bundled against. */
@@ -37,20 +37,31 @@ export function isDesignRenderingInput(relativePath: string): boolean {
37
37
  return true;
38
38
  }
39
39
 
40
- /** sha256 over every rendering input under `root` (sorted relative path + contents) — the identity of a design tree as it paints. */
40
+ /**
41
+ * sha256 over every rendering input under `root` (sorted relative path +
42
+ * contents) — the identity of a design tree as it paints.
43
+ *
44
+ * A function of the TREE, not of the spelling of `root`: the relative keys
45
+ * are taken against the RESOLVED root, so `…/design/src`, `…/design/src/` and
46
+ * `…/ui-gen/../design/src` hash the same bytes. Before this the key was
47
+ * sliced by the unresolved root's length while each entry had already been
48
+ * resolved, so an un-normalised root produced a different hash for the same
49
+ * tree — a phantom design skew for any caller composing the path.
50
+ */
41
51
  export function designTreeSha256(root: string): string {
52
+ const base = resolve(root);
42
53
  const files: string[] = [];
43
54
  const walk = (dir: string): void => {
44
55
  for (const name of readdirSync(dir).sort()) {
45
56
  const full = resolve(dir, name);
46
57
  if (statSync(full).isDirectory()) walk(full);
47
- else if (isDesignRenderingInput(full.slice(root.length + 1))) files.push(full);
58
+ else if (isDesignRenderingInput(relative(base, full))) files.push(full);
48
59
  }
49
60
  };
50
- walk(root);
61
+ walk(base);
51
62
  const h = createHash('sha256');
52
63
  for (const f of files) {
53
- h.update(f.slice(root.length + 1) + '\n');
64
+ h.update(relative(base, f) + '\n');
54
65
  h.update(readFileSync(f));
55
66
  h.update('\n');
56
67
  }
@@ -123,6 +123,24 @@ export interface CriterionCoverage {
123
123
  readonly reason?: string;
124
124
  }
125
125
 
126
+ /**
127
+ * Coverage of the screenshot JUDGE leg for one eval round (ggui#1221) — a
128
+ * different surface from the `visual` CRITERION in `criteriaCoverage` (the
129
+ * LLM evaluator's read of the source). `ran` = a frame was captured and
130
+ * judged; `skipped` = the leg was configured but produced no verdict
131
+ * (no browser, a launch or in-page failure, an unbundlable component, an
132
+ * unparsable judge answer) with `reason` naming why; `not-applicable` =
133
+ * the round had no visual leg configured. Before this row a skipped leg
134
+ * left NO trace on the result — `{ issues: [] }` read exactly like a
135
+ * clean run, and every bootstrap mint on a browserless runtime "passed"
136
+ * its visual round that way.
137
+ */
138
+ export interface VisualCoverage {
139
+ readonly status: CriterionRunStatus;
140
+ /** Populated for `skipped` / `not-applicable` — why no verdict. */
141
+ readonly reason?: string;
142
+ }
143
+
126
144
  /**
127
145
  * The static criteria `runLLMEvaluation` runs, in stamp order — the
128
146
  * ONE list both the evaluator's coverage stamp and the harness's
@@ -185,6 +203,13 @@ export interface EvalResult {
185
203
  * never here — this object is serialized into reports.
186
204
  */
187
205
  visual?: VisualEvalSummary;
206
+ /**
207
+ * Whether the screenshot judge leg ran, was skipped (with the reason) or
208
+ * was not configured — ALWAYS stamped by the harness's eval round
209
+ * (ggui#1221), so a consumer can tell a clean visual run from a leg
210
+ * that silently could not run. See {@link VisualCoverage}.
211
+ */
212
+ visualCoverage?: VisualCoverage;
188
213
  }
189
214
 
190
215
  /** One canvas's visual verdict, PNG-free — see `EvalResult.visual`. */
@@ -227,6 +252,21 @@ export interface CanvasVisualSummary {
227
252
  fit?: 'fill';
228
253
  }
229
254
 
255
+ /**
256
+ * ggui#1195 — the fit reading on the canvas whose policy FAILS an overflow
257
+ * (the inline chat card): the box it was judged at (`ceiling` — the order's
258
+ * declared viewport when one was carried, else the class box), whether that
259
+ * box was declared, and how many px of content sit below it (0 = fits).
260
+ * Present only when such a canvas was judged with a measurable height, so
261
+ * the bar and the ledger read one number for "does the hello fit".
262
+ */
263
+ export interface VisualFitStamp {
264
+ readonly canvas: CanvasClass;
265
+ readonly ceiling: { readonly width: number; readonly height: number };
266
+ readonly declared: boolean;
267
+ readonly overflowPx: number;
268
+ }
269
+
230
270
  /** The visual leg's per-canvas summary — see `EvalResult.visual`. */
231
271
  export interface VisualEvalSummary {
232
272
  /** Mean of the canvas scores (rounded). */
@@ -234,6 +274,8 @@ export interface VisualEvalSummary {
234
274
  /** Every canvas passed. */
235
275
  passed: boolean;
236
276
  canvases: CanvasVisualSummary[];
277
+ /** ggui#1195 — see {@link VisualFitStamp}; absent when no fail-policy canvas was judged. */
278
+ fit?: VisualFitStamp;
237
279
  /** The design tree the judge painted with (ggui#1042): `src` + `srcSha256` — `design@judge`. */
238
280
  design?: { readonly src: string; readonly srcSha256: string };
239
281
  /** The mode the judge's tokens were composed in, when the caller said (ggui#1076). */