@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.
- package/dist/adapters/base.d.ts +7 -0
- package/dist/adapters/base.js +1 -0
- package/dist/adapters/base.js.map +1 -1
- package/dist/adapters/generation-dispatch.d.ts +17 -3
- package/dist/adapters/generation-dispatch.js +1078 -3464
- package/dist/adapters/generation-dispatch.js.map +1 -1
- package/dist/adapters/index.js +60 -3
- package/dist/adapters/index.js.map +1 -1
- package/dist/advanced/index.d.ts +4 -4
- package/dist/advanced/index.js +1346 -3557
- package/dist/advanced/index.js.map +1 -1
- package/dist/blueprint-validator.js +10 -3
- package/dist/blueprint-validator.js.map +1 -1
- package/dist/boilerplate.js +2 -2
- 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-CxPvudbs.d.ts → create-ui-generator-BEkzEtNz.d.ts} +2 -2
- package/dist/{dispatch-N8wdNOX-.d.ts → dispatch-DJNpQ7DI.d.ts} +1 -1
- package/dist/evaluation/axis-checks/index.d.ts +3 -3
- package/dist/evaluation/axis-checks/index.js.map +1 -1
- package/dist/evaluation/axis-checks/registry.d.ts +1 -1
- package/dist/evaluation/index.d.ts +38 -6
- package/dist/evaluation/index.js +670 -3374
- package/dist/evaluation/index.js.map +1 -1
- package/dist/harness/check/runtime-render/index.d.ts +2 -2
- package/dist/harness/check/runtime-render/index.js +10 -3
- package/dist/harness/check/runtime-render/index.js.map +1 -1
- package/dist/harness/check/runtime-render/render-check-worker.js +5 -3
- package/dist/harness/check/runtime-render/render-check-worker.js.map +1 -1
- package/dist/harness/index.d.ts +2 -2
- package/dist/harness/index.js +587 -30
- package/dist/harness/index.js.map +1 -1
- package/dist/harness/llm-trace-sink.d.ts +7 -1
- package/dist/harness/llm-trace-sink.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 +11 -2
- package/dist/harness/runtime.js +3 -3
- package/dist/harness/runtime.js.map +1 -1
- package/dist/harness/types-public.d.ts +1 -1
- package/dist/harness/types-public.js +2 -2
- package/dist/harness/types-public.js.map +1 -1
- package/dist/harness/validator-trace-sink.d.ts +1 -1
- package/dist/index.d.ts +23 -7
- package/dist/index.js +1346 -3557
- package/dist/index.js.map +1 -1
- package/dist/{llm-router-D7fjBhtr.d.ts → llm-router-DwwUK1bz.d.ts} +16 -3
- package/dist/patch.d.ts +1 -1
- package/dist/providers/index.js +5 -4
- package/dist/providers/index.js.map +1 -1
- package/dist/{render-check-WfOVO9kU.d.ts → render-check-C0CqVceF.d.ts} +17 -0
- package/dist/{types-public-DKR_h70H.d.ts → types-public-mM_-1Yk3.d.ts} +59 -1
- 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 +16 -12
- package/src/adapters/base.ts +8 -0
- package/src/adapters/claude/claude-code-login.ts +128 -0
- package/src/adapters/claude/sdk.ts +9 -1
- package/src/adapters/generation-dispatch.ts +19 -2
- package/src/adapters/openai/tokens.ts +18 -0
- package/src/adapters/provider-router.ts +28 -0
- package/src/boilerplate/generate.ts +19 -4
- package/src/boilerplate/hard-sections.ts +1 -1
- package/src/boilerplate/templates/layouts/chat-mobile.tsx.tmpl +7 -3
- package/src/boilerplate/templates/layouts/chat-universal.tsx.tmpl +7 -3
- package/src/contract-context.ts +1 -1
- package/src/create-ui-generator.ts +49 -10
- package/src/evaluation/design-identity.ts +16 -5
- package/src/evaluation/index.ts +1 -0
- package/src/evaluation/llm-evaluator.ts +40 -13
- package/src/evaluation/types-public.ts +58 -0
- package/src/evaluation/visual-evaluator.ts +240 -30
- package/src/generator-build.ts +158 -0
- package/src/harness/check/runtime-render/adapter.ts +9 -0
- package/src/harness/check/runtime-render/render-check.ts +26 -5
- package/src/harness/claude-code-login-query.ts +529 -0
- package/src/harness/coding/assemble-result.ts +18 -2
- 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 +338 -38
- package/src/harness/enforced-coding.ts +20 -0
- package/src/harness/llm-router.ts +390 -55
- package/src/harness/llm-trace-sink.ts +7 -1
- package/src/harness/result-types.ts +14 -0
- package/src/harness/runtime.ts +6 -0
- package/src/index.ts +2 -1
- package/src/providers/openai.ts +31 -19
- 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-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.
|
|
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/
|
|
192
|
-
"@ggui-ai/
|
|
193
|
-
"@ggui-ai/protocol": "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/
|
|
233
|
-
"@ggui-ai/
|
|
234
|
-
"@ggui-ai/
|
|
235
|
-
"@ggui-ai/ui-visual-tester": "0.
|
|
236
|
-
"@ggui-ai/wire": "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/
|
|
252
|
-
"@ggui-ai/
|
|
253
|
-
"@ggui-ai/
|
|
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"
|
package/src/adapters/base.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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.
|
|
259
|
-
//
|
|
260
|
-
//
|
|
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
|
-
<
|
|
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>
|
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';
|
|
@@ -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
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
}
|
|
310
|
-
|
|
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
|
-
/**
|
|
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(
|
|
58
|
+
else if (isDesignRenderingInput(relative(base, full))) files.push(full);
|
|
48
59
|
}
|
|
49
60
|
};
|
|
50
|
-
walk(
|
|
61
|
+
walk(base);
|
|
51
62
|
const h = createHash('sha256');
|
|
52
63
|
for (const f of files) {
|
|
53
|
-
h.update(
|
|
64
|
+
h.update(relative(base, f) + '\n');
|
|
54
65
|
h.update(readFileSync(f));
|
|
55
66
|
h.update('\n');
|
|
56
67
|
}
|