@ggui-ai/ui-gen 0.20.0 → 0.22.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 (93) 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 +1078 -3464
  6. package/dist/adapters/generation-dispatch.js.map +1 -1
  7. package/dist/adapters/index.js +60 -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 +1346 -3557
  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-BEkzEtNz.d.ts} +2 -2
  19. package/dist/{dispatch-N8wdNOX-.d.ts → dispatch-DJNpQ7DI.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 +670 -3374
  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 +587 -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/prompts.js +1 -1
  37. package/dist/harness/prompts.js.map +1 -1
  38. package/dist/harness/result-types.d.ts +15 -2
  39. package/dist/harness/result-types.js.map +1 -1
  40. package/dist/harness/runtime.d.ts +11 -2
  41. package/dist/harness/runtime.js +3 -3
  42. package/dist/harness/runtime.js.map +1 -1
  43. package/dist/harness/types-public.d.ts +1 -1
  44. package/dist/harness/types-public.js +2 -2
  45. package/dist/harness/types-public.js.map +1 -1
  46. package/dist/harness/validator-trace-sink.d.ts +1 -1
  47. package/dist/index.d.ts +23 -7
  48. package/dist/index.js +1346 -3557
  49. package/dist/index.js.map +1 -1
  50. package/dist/{llm-router-D7fjBhtr.d.ts → llm-router-DwwUK1bz.d.ts} +16 -3
  51. package/dist/patch.d.ts +1 -1
  52. package/dist/providers/index.js +5 -4
  53. package/dist/providers/index.js.map +1 -1
  54. package/dist/{render-check-WfOVO9kU.d.ts → render-check-C0CqVceF.d.ts} +17 -0
  55. package/dist/{types-public-DKR_h70H.d.ts → types-public-mM_-1Yk3.d.ts} +59 -1
  56. package/dist/validation/index.d.ts +1 -1
  57. package/dist/validation/index.js +1 -1
  58. package/dist/validation/index.js.map +1 -1
  59. package/dist/workflows.d.ts +1 -1
  60. package/package.json +16 -12
  61. package/src/adapters/base.ts +8 -0
  62. package/src/adapters/claude/claude-code-login.ts +128 -0
  63. package/src/adapters/claude/sdk.ts +9 -1
  64. package/src/adapters/generation-dispatch.ts +19 -2
  65. package/src/adapters/openai/tokens.ts +18 -0
  66. package/src/adapters/provider-router.ts +28 -0
  67. package/src/boilerplate/generate.ts +19 -4
  68. package/src/boilerplate/hard-sections.ts +1 -1
  69. package/src/boilerplate/templates/layouts/chat-mobile.tsx.tmpl +7 -3
  70. package/src/boilerplate/templates/layouts/chat-universal.tsx.tmpl +7 -3
  71. package/src/contract-context.ts +1 -1
  72. package/src/create-ui-generator.ts +49 -10
  73. package/src/evaluation/design-identity.ts +16 -5
  74. package/src/evaluation/index.ts +1 -0
  75. package/src/evaluation/llm-evaluator.ts +40 -13
  76. package/src/evaluation/types-public.ts +58 -0
  77. package/src/evaluation/visual-evaluator.ts +240 -30
  78. package/src/generator-build.ts +158 -0
  79. package/src/harness/check/runtime-render/adapter.ts +9 -0
  80. package/src/harness/check/runtime-render/render-check.ts +26 -5
  81. package/src/harness/claude-code-login-query.ts +529 -0
  82. package/src/harness/coding/assemble-result.ts +18 -2
  83. package/src/harness/coding/generate-task-runner.ts +75 -36
  84. package/src/harness/coding/run-coding-turn.ts +9 -0
  85. package/src/harness/coding/run-eval-round.ts +338 -38
  86. package/src/harness/enforced-coding.ts +20 -0
  87. package/src/harness/llm-router.ts +390 -55
  88. package/src/harness/llm-trace-sink.ts +7 -1
  89. package/src/harness/result-types.ts +14 -0
  90. package/src/harness/runtime.ts +6 -0
  91. package/src/index.ts +2 -1
  92. package/src/providers/openai.ts +31 -19
  93. 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-mM_-1Yk3.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.22.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/mcp-server-core": "0.22.0",
193
+ "@ggui-ai/project-config": "0.22.0",
194
+ "@ggui-ai/protocol": "0.22.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.22.0",
237
+ "@ggui-ai/sandbox": "0.22.0",
238
+ "@ggui-ai/gadgets": "0.22.0",
239
+ "@ggui-ai/ui-visual-tester": "0.22.0",
240
+ "@ggui-ai/wire": "0.22.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.22.0",
256
+ "@ggui-ai/project-config": "0.22.0",
257
+ "@ggui-ai/protocol": "0.22.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,128 @@
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 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).
18
+ * 3. {@link claudeCodeLoginQueryOptions} — the SDK options that keep the
19
+ * run bounded: no built-in tool (`tools: []`), no `~/.claude` settings,
20
+ * hooks or MCP servers leaking in (`settingSources: []`). Not
21
+ * `--bare`, either: on the SDK's bundled binary `-p` reads the login
22
+ * store as it always has, so no flag is needed — and the flag it
23
+ * would be (`--no-bare`) is one that binary rejects; see the note on
24
+ * the function below.
25
+ *
26
+ * Opt-in, off by default; the default stays a provider key. The copy that
27
+ * names what this does lives with the CLI flag that enables it.
28
+ */
29
+
30
+ import type { Options } from "@anthropic-ai/claude-agent-sdk";
31
+
32
+ /** Sentinel credential: "no key — use the machine's Claude Code login". */
33
+ export const CLAUDE_CODE_LOGIN_CREDENTIAL = "claude-code-login" as const;
34
+
35
+ /**
36
+ * Every env name the spawned binary would read as a credential or an
37
+ * endpoint override ahead of its login. Stripped whole, never rewritten.
38
+ */
39
+ export const PROVIDER_KEY_ENV_NAMES = [
40
+ "ANTHROPIC_API_KEY",
41
+ "CLAUDE_API_KEY",
42
+ "ANTHROPIC_AUTH_TOKEN",
43
+ "ANTHROPIC_BASE_URL",
44
+ ] as const;
45
+
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(
101
+ env: Readonly<Record<string, string | undefined>>
102
+ ): Record<string, string> {
103
+ const out: Record<string, string> = {};
104
+ for (const [k, v] of Object.entries(env)) {
105
+ if (v !== undefined && (LOGIN_CHILD_ENV.has(k) || k.startsWith("LC_"))) out[k] = v;
106
+ }
107
+ return out;
108
+ }
109
+
110
+ /**
111
+ * The SDK `query()` options a login-path run is pinned to — typed as the
112
+ * SDK's own `Options` members, so a change in the SDK's shape is a compile
113
+ * error here and never a silently ignored key.
114
+ *
115
+ * NOT pinned: `--no-bare`. The SDK spawns its BUNDLED binary (2.1.229 for
116
+ * SDK 0.3.229), which has no bare mode and rejects the flag outright
117
+ * (`error: unknown option '--no-bare'`, measured 2026-09-21 on the first
118
+ * live turn of the login client); the machine's own `claude` (2.1.274)
119
+ * accepts it, which is where the earlier probe measured it. On the
120
+ * bundled binary `-p` reads the login store as it always has. If a later
121
+ * SDK bundles a binary where bare becomes the `-p` default, the flag
122
+ * comes back here — with the SDK version bump, measured again.
123
+ */
124
+ export type ClaudeCodeLoginQueryOptions = Required<Pick<Options, "tools" | "settingSources">>;
125
+
126
+ export function claudeCodeLoginQueryOptions(): ClaudeCodeLoginQueryOptions {
127
+ return { tools: [], settingSources: [] };
128
+ }
@@ -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, loginChildEnv } from './claude-code-login';
16
17
 
17
18
  export class ClaudeSdkAdapter extends GeneratorAdapter {
18
19
  readonly provider: ProviderName = 'claude';
@@ -40,7 +41,12 @@ 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/#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.
48
+ const login = cfg.claudeCodeLogin === true;
49
+ const env = login ? loginChildEnv(buildEnv(cfg.env)) : buildEnv(cfg.env);
44
50
 
45
51
  const artifacts = createArtifacts();
46
52
  const onProgress = cfg.onProgress;
@@ -72,6 +78,8 @@ export class ClaudeSdkAdapter extends GeneratorAdapter {
72
78
  allowDangerouslySkipPermissions: true,
73
79
  ...(cfg.cliPath && { pathToClaudeCodeExecutable: cfg.cliPath }),
74
80
  ...(cfg.stderr && { stderr: cfg.stderr }),
81
+ // ggui#1185 — tool-less, config-less, non-bare; the model is pinned above.
82
+ ...(login ? claudeCodeLoginQueryOptions() : {}),
75
83
  },
76
84
  })) {
77
85
  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
  /**
@@ -1,3 +1,7 @@
1
- <Card padding="sm" shadow="sm">
2
- {/* implement your UI here */}
3
- </Card>
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
- <Card padding="sm" shadow="sm">
2
- {/* implement your UI here */}
3
- </Card>
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>
@@ -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 parent bubble). Do NOT set max-width.`,
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';
@@ -298,16 +300,25 @@ export function createUiGenerator(
298
300
  // structurally into the dispatch call instead. `route.env`
299
301
  // already carries exactly the key(s) that would have gone into
300
302
  // `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;
303
+ //
304
+ // ggui#1185 — the login path has NO key to write anywhere: the
305
+ // route decision `auth: 'claude-code-login'` is carried as
306
+ // `routeOverride.claudeCodeLogin` in BOTH env modes, because the
307
+ // flag is the only way the decision reaches `createAgent` /
308
+ // `createVisionAgent` (which select the login client on it).
309
+ const routeOverride =
310
+ route.auth === 'claude-code-login'
311
+ ? { claudeCodeLogin: true }
312
+ : disableEnvMutation
313
+ ? {
314
+ apiKey:
315
+ route.env['ANTHROPIC_API_KEY'] ??
316
+ route.env['OPENAI_API_KEY'] ??
317
+ route.env['GEMINI_API_KEY'] ??
318
+ route.env['OPENROUTER_API_KEY'],
319
+ useBedrock: route.env['CLAUDE_CODE_USE_BEDROCK'] === '1',
320
+ }
321
+ : undefined;
311
322
 
312
323
  try {
313
324
  const tools = createGeneratorTools({
@@ -330,6 +341,7 @@ export function createUiGenerator(
330
341
  // Ignored by the constrained prompt (its shell/screen
331
342
  // descriptors stay byte-identical).
332
343
  const canvas = rendering ? canvasForRenderingContext(rendering) : undefined;
344
+ const shellType = harnessShellForRendering(rendering);
333
345
  const promptWithVariance = injectVariance(
334
346
  promptWithRendering,
335
347
  input.variance,
@@ -396,6 +408,8 @@ export function createUiGenerator(
396
408
  ...(onRetry !== undefined ? { onRetry } : {}),
397
409
  ...(designMode !== undefined ? { designMode } : {}),
398
410
  ...(canvas !== undefined ? { canvas } : {}),
411
+ // ggui#1279 — the shell the declared rendering composes for (chat only; see harnessShellForRendering).
412
+ ...(shellType !== undefined ? { shellType } : {}),
399
413
  ...(input.profile !== undefined ? { profile: input.profile } : {}),
400
414
  // Fixture props reach the in-loop probe + visual round only (never the prompt, never identity).
401
415
  ...(input.fixtureProps !== undefined ? { fixtureProps: input.fixtureProps } : {}),
@@ -412,6 +426,8 @@ export function createUiGenerator(
412
426
  latencyMs: Date.now() - startedAt,
413
427
  cacheHit: false,
414
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),
415
431
  // Prompt-cache counters are provider-specific (Claude reports
416
432
  // them; others omit). Pass through truthfully — absent on the
417
433
  // adapter result stays absent here, never defaulted to 0.
@@ -452,6 +468,8 @@ export function createUiGenerator(
452
468
  outputTokens: 0,
453
469
  latencyMs: Date.now() - startedAt,
454
470
  cacheHit: false,
471
+ // ggui#1280 — a failed generation still names the build that failed.
472
+ build: generatorBuild(designMode ?? DEFAULT_DESIGN_MODE),
455
473
  },
456
474
  };
457
475
  } finally {
@@ -514,6 +532,27 @@ function mapLlmProviderToDispatchProvider(provider: LlmProvider): ProviderName {
514
532
  }
515
533
  }
516
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
+
517
556
  function mapRendering(
518
557
  rendering: UiGenerateInput['rendering'],
519
558
  ): RenderingContext | undefined {
@@ -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
  }
@@ -22,6 +22,7 @@ export type {
22
22
  EvalOutcome,
23
23
  EvalCategory,
24
24
  EvalResult,
25
+ ContractFeedbackRecord,
25
26
  VisualEvalSummary,
26
27
  CanvasJudgeRecord,
27
28
  CanvasVisualSummary,