@ggui-ai/ui-gen 0.26.0 → 0.27.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/{adapter-BJwn74rx.d.ts → adapter-h4_V69LI.d.ts} +1 -1
- package/dist/adapters/base.d.ts +6 -1
- package/dist/adapters/base.js.map +1 -1
- package/dist/adapters/generation-dispatch.d.ts +2 -2
- package/dist/adapters/generation-dispatch.js +61 -10
- package/dist/adapters/generation-dispatch.js.map +1 -1
- package/dist/adapters/index.js +36 -14
- package/dist/adapters/index.js.map +1 -1
- package/dist/advanced/index.d.ts +4 -4
- package/dist/advanced/index.js +94 -19
- package/dist/advanced/index.js.map +1 -1
- package/dist/boilerplate.js +4 -6
- package/dist/boilerplate.js.map +1 -1
- package/dist/check/index.d.ts +1 -7
- package/dist/check/index.js +50 -4
- package/dist/check/index.js.map +1 -1
- package/dist/{create-ui-generator-N4gRXpcL.d.ts → create-ui-generator-BrZHF0cf.d.ts} +2 -2
- package/dist/{dispatch-BUoXRj_r.d.ts → dispatch-B07TI30d.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 +22 -12
- package/dist/evaluation/index.js +36 -10
- package/dist/evaluation/index.js.map +1 -1
- package/dist/evaluation/types.d.ts +1 -1
- package/dist/harness/check/runtime-render/index.d.ts +3 -3
- package/dist/harness/index.d.ts +1 -1
- package/dist/harness/index.js +4 -6
- package/dist/harness/index.js.map +1 -1
- package/dist/harness/prompts.js +1 -4
- package/dist/harness/prompts.js.map +1 -1
- package/dist/harness/result-types.d.ts +2 -2
- package/dist/harness/runtime.d.ts +1 -1
- package/dist/harness/runtime.js +6 -7
- package/dist/harness/runtime.js.map +1 -1
- package/dist/harness/types-public.d.ts +1 -1
- package/dist/harness/types-public.js +4 -6
- package/dist/harness/types-public.js.map +1 -1
- package/dist/harness/validator-trace-sink.d.ts +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.js +94 -19
- package/dist/index.js.map +1 -1
- package/dist/patch.d.ts +1 -1
- package/dist/{types-public-B7wn4ce5.d.ts → types-public-h8IMnYuh.d.ts} +4 -0
- package/dist/{visual-evaluator-C9L-o6ap.d.ts → visual-evaluator-C2strxqD.d.ts} +1 -1
- package/dist/workflows.d.ts +1 -1
- package/package.json +12 -12
- package/src/adapters/base.ts +6 -1
- package/src/adapters/claude/message-parser.ts +5 -6
- package/src/adapters/claude/sdk-tool-pins.ts +44 -0
- package/src/adapters/claude/sdk.ts +3 -3
- package/src/adapters/claude/source-capture.ts +20 -0
- package/src/boilerplate/system-prompt.ts +3 -2
- package/src/check/run-tier0.ts +61 -0
- package/src/design-system-docs.ts +2 -1
- package/src/evaluation/criteria/resolve.ts +19 -1
- package/src/evaluation/loop.ts +25 -4
- package/src/evaluation/message-parsing.ts +11 -12
- package/src/evaluation/types-public.ts +4 -0
- package/src/taught-tokens.ts +5 -6
package/dist/patch.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { PatchFn } from './harness/types-public.js';
|
|
|
2
2
|
import '@ggui-ai/protocol';
|
|
3
3
|
import './styling-profile-CxfvV48g.js';
|
|
4
4
|
import './axes-CKVCzR_x.js';
|
|
5
|
-
import './types-public-
|
|
5
|
+
import './types-public-h8IMnYuh.js';
|
|
6
6
|
import './llm.js';
|
|
7
7
|
import '@ggui-ai/mcp-server-core';
|
|
8
8
|
import './design-mode-DGWN_Gkm.js';
|
|
@@ -599,6 +599,10 @@ interface CanvasJudgeRecord {
|
|
|
599
599
|
/**
|
|
600
600
|
* ggui#1687 — the model asked for, and every model the provider said answered this canvas's calls (scoring and
|
|
601
601
|
* criteria), deduplicated. `served` is empty when no provider named one: unknown, never "the requested model".
|
|
602
|
+
* When the request names an alias and the provider echoes the alias (measured on a hosted route, 125 of 125
|
|
603
|
+
* canvases), `served` confirms the alias was honoured but cannot tell two snapshots behind it apart. A judge that
|
|
604
|
+
* must is pinned to a dated snapshot id instead. With the alias, a snapshot change shows only as a step on a date in
|
|
605
|
+
* the judge's pooled within-call spread, read by week. That is a read someone runs, not a monitor.
|
|
602
606
|
*/
|
|
603
607
|
readonly models?: {
|
|
604
608
|
readonly requested: string;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { h as CriteriaContext, i as CriteriaChroma, j as CanvasJudgeRecord, k as CriteriaBlock, l as CanvasPresentationOutcome, n as CanvasHostPresentation, b as AgentConfig, L as LLMResponse, J as JudgeSalvageCounts, E as EvalIssue, o as VisualEvalSummary, p as VisualCoverage } from './types-public-
|
|
1
|
+
import { h as CriteriaContext, i as CriteriaChroma, j as CanvasJudgeRecord, k as CriteriaBlock, l as CanvasPresentationOutcome, n as CanvasHostPresentation, b as AgentConfig, L as LLMResponse, J as JudgeSalvageCounts, E as EvalIssue, o as VisualEvalSummary, p as VisualCoverage } from './types-public-h8IMnYuh.js';
|
|
2
2
|
import { G as GenerationProfileInput } from './styling-profile-CxfvV48g.js';
|
|
3
3
|
import { C as CriteriaBank } from './bank-Ctdp3FUA.js';
|
|
4
4
|
import { DataContract, JsonObject } from '@ggui-ai/protocol';
|
package/dist/workflows.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { C as Classification } from './axes-CKVCzR_x.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-h8IMnYuh.js';
|
|
6
6
|
import './llm.js';
|
|
7
7
|
import '@ggui-ai/mcp-server-core';
|
|
8
8
|
import './design-mode-DGWN_Gkm.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ggui-ai/ui-gen",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.0",
|
|
4
4
|
"description": "UI generation harness for ggui — the LLM system prompt, multi-turn coding loop, and evaluation engine that turn a DataContract into a React component. Implements the UiGenerator contract from @ggui-ai/mcp-server-core; consumed by @ggui-ai/mcp-server.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"keywords": [
|
|
@@ -189,9 +189,9 @@
|
|
|
189
189
|
"puppeteer-core": "^24.40.0 || ^25.0.0",
|
|
190
190
|
"react": "^18.0.0 || ^19.0.0",
|
|
191
191
|
"react-dom": "^18.0.0 || ^19.0.0",
|
|
192
|
-
"@ggui-ai/mcp-server-core": "0.
|
|
193
|
-
"@ggui-ai/
|
|
194
|
-
"@ggui-ai/
|
|
192
|
+
"@ggui-ai/mcp-server-core": "0.27.0",
|
|
193
|
+
"@ggui-ai/project-config": "0.27.0",
|
|
194
|
+
"@ggui-ai/protocol": "0.27.0"
|
|
195
195
|
},
|
|
196
196
|
"peerDependenciesMeta": {
|
|
197
197
|
"@google/adk": {
|
|
@@ -233,11 +233,11 @@
|
|
|
233
233
|
"openai": "^7.9.0",
|
|
234
234
|
"typescript": "^5.0.0",
|
|
235
235
|
"zod": "^4.3.6",
|
|
236
|
-
"@ggui-ai/design": "0.
|
|
237
|
-
"@ggui-ai/
|
|
238
|
-
"@ggui-ai/
|
|
239
|
-
"@ggui-ai/
|
|
240
|
-
"@ggui-ai/wire": "0.
|
|
236
|
+
"@ggui-ai/design": "0.27.0",
|
|
237
|
+
"@ggui-ai/sandbox": "0.27.0",
|
|
238
|
+
"@ggui-ai/gadgets": "0.27.0",
|
|
239
|
+
"@ggui-ai/ui-visual-tester": "0.27.0",
|
|
240
|
+
"@ggui-ai/wire": "0.27.0"
|
|
241
241
|
},
|
|
242
242
|
"devDependencies": {
|
|
243
243
|
"@google/adk": "^2.0.0",
|
|
@@ -252,9 +252,9 @@
|
|
|
252
252
|
"react-dom": "^19.0.0",
|
|
253
253
|
"tsup": "^8.5.0",
|
|
254
254
|
"vitest": "^3.2.6",
|
|
255
|
-
"@ggui-ai/
|
|
256
|
-
"@ggui-ai/
|
|
257
|
-
"@ggui-ai/protocol": "0.
|
|
255
|
+
"@ggui-ai/mcp-server-core": "0.27.0",
|
|
256
|
+
"@ggui-ai/project-config": "0.27.0",
|
|
257
|
+
"@ggui-ai/protocol": "0.27.0"
|
|
258
258
|
},
|
|
259
259
|
"engines": {
|
|
260
260
|
"node": ">=22.0.0"
|
package/src/adapters/base.ts
CHANGED
|
@@ -45,8 +45,13 @@ export interface ClaudeSdkConfig extends AdapterConfig {
|
|
|
45
45
|
env?: Record<string, string>;
|
|
46
46
|
/** MCP server configs for Claude Agent SDK (keyed by name) */
|
|
47
47
|
mcpServers?: Record<string, McpServerConfig>;
|
|
48
|
-
/**
|
|
48
|
+
/** Tool names to approve. Absent → every tool of every server in `mcpServers` (ggui#1714). */
|
|
49
49
|
allowedTools?: string[];
|
|
50
|
+
/**
|
|
51
|
+
* CLI built-in tools to offer the model (e.g. `['Write']`). Default: none.
|
|
52
|
+
* An explicit opt-in: a built-in that writes or executes acts on the host (ggui#1714).
|
|
53
|
+
*/
|
|
54
|
+
builtinTools?: readonly string[];
|
|
50
55
|
/** Subagent definitions for Claude Agent SDK */
|
|
51
56
|
agents?: Record<string, AgentDefinition>;
|
|
52
57
|
/** Custom CLI path for Docker deployments */
|
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
SDKUserMessage,
|
|
14
14
|
} from '@anthropic-ai/claude-agent-sdk';
|
|
15
15
|
import type { JsonObject } from '@ggui-ai/protocol';
|
|
16
|
+
import { sourceCodeFromToolUse } from './source-capture';
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* Mutable accumulator for artifacts extracted from the SDK message stream.
|
|
@@ -197,12 +198,10 @@ function extractSourceCode(message: SDKMessage, artifacts: MessageArtifacts): vo
|
|
|
197
198
|
if (!Array.isArray(content)) return;
|
|
198
199
|
|
|
199
200
|
for (const block of content) {
|
|
200
|
-
if (block.type
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
}
|
|
205
|
-
}
|
|
201
|
+
if (block.type !== 'tool_use') continue;
|
|
202
|
+
// ggui#1714 — the rule lives in source-capture.ts, shared with the evaluation loop.
|
|
203
|
+
const value = sourceCodeFromToolUse(block.name, block.input);
|
|
204
|
+
if (value !== undefined) artifacts.sourceCode = value;
|
|
206
205
|
}
|
|
207
206
|
}
|
|
208
207
|
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// packages/ui-gen/src/adapters/claude/sdk-tool-pins.ts
|
|
2
|
+
//
|
|
3
|
+
// ggui#1714 — the tool surface of every Agent SDK `query()` this package runs.
|
|
4
|
+
//
|
|
5
|
+
// Left to its defaults, a `query()` session offers the model the CLI's whole
|
|
6
|
+
// built-in set (Bash, Edit, Read, Write, WebFetch, Task, …) and loads the host
|
|
7
|
+
// user's own Claude configuration (plugins, MCP servers from `~/.claude`).
|
|
8
|
+
// `allowedTools` does not narrow that: it only approves the names it lists.
|
|
9
|
+
// Measured on SDK 0.3.229 from the session's `system`/`init` message:
|
|
10
|
+
//
|
|
11
|
+
// as before this file → every built-in + the caller's tools
|
|
12
|
+
// `tools: []` alone → the caller's tools + a user-level plugin server
|
|
13
|
+
// `tools: []` + `settingSources: []` → the caller's tools only
|
|
14
|
+
//
|
|
15
|
+
// So the pins are both, on every path. With them in place no permission
|
|
16
|
+
// bypass is needed: a tool named in `allowedTools` runs, and a server-level
|
|
17
|
+
// entry (`mcp__<server>`) approves every tool of that server (both measured
|
|
18
|
+
// on a live turn; with no entry the call is denied and the run reports it).
|
|
19
|
+
|
|
20
|
+
import type { McpServerConfig, Options } from '@anthropic-ai/claude-agent-sdk';
|
|
21
|
+
|
|
22
|
+
export type SdkToolPins = Required<Pick<Options, 'tools' | 'settingSources' | 'allowedTools'>>;
|
|
23
|
+
|
|
24
|
+
export interface SdkToolPinInput {
|
|
25
|
+
/** The MCP servers the session gets, keyed by name. */
|
|
26
|
+
readonly mcpServers?: Record<string, McpServerConfig>;
|
|
27
|
+
/** Tool names to approve. Absent → every tool of every server in `mcpServers`. */
|
|
28
|
+
readonly allowedTools?: readonly string[];
|
|
29
|
+
/**
|
|
30
|
+
* The CLI built-ins to offer (e.g. `['Write']`) — an explicit opt-in.
|
|
31
|
+
* Each one is also approved, so it runs without a prompt; a built-in that
|
|
32
|
+
* writes or executes acts on the host with the session's permissions.
|
|
33
|
+
*/
|
|
34
|
+
readonly builtinTools?: readonly string[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function sdkToolPins(input: SdkToolPinInput): SdkToolPins {
|
|
38
|
+
const builtins = [...(input.builtinTools ?? [])];
|
|
39
|
+
const approved =
|
|
40
|
+
input.allowedTools !== undefined
|
|
41
|
+
? [...input.allowedTools]
|
|
42
|
+
: Object.keys(input.mcpServers ?? {}).map((name) => `mcp__${name}`);
|
|
43
|
+
return { tools: builtins, settingSources: [], allowedTools: [...approved, ...builtins] };
|
|
44
|
+
}
|
|
@@ -14,6 +14,7 @@ import type { AdapterResult, ProviderName, AdapterMode } from '../types';
|
|
|
14
14
|
import { createArtifacts, processMessage } from './message-parser';
|
|
15
15
|
import { createToolMcpServer } from './tool-bridge';
|
|
16
16
|
import { claudeCodeLoginQueryOptions, loginChildEnv } from './claude-code-login';
|
|
17
|
+
import { sdkToolPins } from './sdk-tool-pins';
|
|
17
18
|
|
|
18
19
|
export class ClaudeSdkAdapter extends GeneratorAdapter {
|
|
19
20
|
readonly provider: ProviderName = 'claude';
|
|
@@ -73,9 +74,8 @@ export class ClaudeSdkAdapter extends GeneratorAdapter {
|
|
|
73
74
|
systemPrompt: params.systemPrompt,
|
|
74
75
|
...(mcpServers && { mcpServers }),
|
|
75
76
|
...(cfg.agents && { agents: cfg.agents }),
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
allowDangerouslySkipPermissions: true,
|
|
77
|
+
// ggui#1714 — no CLI built-ins unless asked for, no host settings, no permission bypass.
|
|
78
|
+
...sdkToolPins({ mcpServers, allowedTools, builtinTools: cfg.builtinTools }),
|
|
79
79
|
...(cfg.cliPath && { pathToClaudeCodeExecutable: cfg.cliPath }),
|
|
80
80
|
...(cfg.stderr && { stderr: cfg.stderr }),
|
|
81
81
|
// ggui#1185 — tool-less, config-less, non-bare; the model is pinned above.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// packages/ui-gen/src/adapters/claude/source-capture.ts
|
|
2
|
+
//
|
|
3
|
+
// ggui#1714 — which tool call carries a component's source, decided once.
|
|
4
|
+
// Both Agent SDK paths (ClaudeSdkAdapter's message parser and the evaluation
|
|
5
|
+
// loop's extractor) read source through this function, so the rule cannot be
|
|
6
|
+
// fixed on one path and left stale on the other.
|
|
7
|
+
//
|
|
8
|
+
// `*__compile_component` → `input.code` (the bridged compile tool)
|
|
9
|
+
// `Write` → `input.content` (a CLI built-in, offered only by opt-in)
|
|
10
|
+
|
|
11
|
+
const COMPILE_TOOL_SUFFIX = '__compile_component';
|
|
12
|
+
|
|
13
|
+
/** The source a tool_use block carries, or undefined when it carries none. */
|
|
14
|
+
export function sourceCodeFromToolUse(name: unknown, input: unknown): string | undefined {
|
|
15
|
+
if (typeof name !== 'string') return undefined;
|
|
16
|
+
const field = name === 'Write' ? 'content' : name.endsWith(COMPILE_TOOL_SUFFIX) ? 'code' : undefined;
|
|
17
|
+
if (field === undefined || typeof input !== 'object' || input === null) return undefined;
|
|
18
|
+
const value: unknown = Reflect.get(input, field);
|
|
19
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
20
|
+
}
|
|
@@ -481,13 +481,14 @@ MANDATORY:
|
|
|
481
481
|
2. NEVER use CSS gradients with custom colors. If you need a gradient: \`linear-gradient(to bottom, var(--ggui-color-primary-500), var(--ggui-color-primary-700))\`.
|
|
482
482
|
3. NEVER invent your own palette. The system provides primary, neutral, success, warning, error, and info — use ONLY these.
|
|
483
483
|
4. NEVER include literal fallback values in ANY token reference — colors, spacing, typography, radius, shadows alike. Write \`var(--ggui-spacing-4)\` bare, never \`var(--ggui-spacing-4, 16px)\`; \`var(--ggui-color-primary-600)\` bare, never \`var(--ggui-color-primary-600, #0284c7)\`. The runtime injects every token; a literal fallback paints the wrong value exactly when the operator's theme matters most.
|
|
484
|
+
5. NEVER set an inline \`border\`, \`borderColor\`, \`borderWidth\` or \`borderStyle\` on a kit control — \`Button\`, \`Input\`, \`Select\`, \`TextArea\`, \`Checkbox\`, \`RadioGroup\`, \`Toggle\`, \`SearchField\`. The control draws its own boundary, and it clears 3:1 on its surface in every theme: an outline chip is \`<Button variant="outline">\` exactly as it comes (its edge is \`controlAccentOutline\`). Where you draw a control-like edge on your own element, use \`var(--ggui-color-controlOutline)\`. A ramp step such as \`primary-200\` as a control's border is nearly invisible (about 1.1:1).
|
|
484
485
|
|
|
485
486
|
Token categories:
|
|
486
487
|
- Brand fills: \`var(--ggui-color-primary-600)\`, \`var(--ggui-color-primary-50)\`
|
|
487
488
|
- Accent text: \`var(--ggui-color-link)\` — eyebrows, taglines, links, labels; readable per mode (never a bare \`primary-<stop>\` as a text color)
|
|
488
489
|
- Text: \`var(--ggui-color-onContainer)\`, \`var(--ggui-color-onSunken)\`
|
|
489
490
|
- Backgrounds: \`var(--ggui-color-container)\`, \`var(--ggui-color-sunken)\`
|
|
490
|
-
- Borders: \`var(--ggui-color-outline)\`
|
|
491
|
+
- Borders: \`var(--ggui-color-outline)\` — dividers and the edges of non-interactive boxes; a control's edge is the kit's (rule 5)
|
|
491
492
|
- Spacing: \`var(--ggui-spacing-4)\`, \`var(--ggui-spacing-6)\`
|
|
492
493
|
- Typography: \`var(--ggui-font-size-sm)\`, \`var(--ggui-font-weight-semibold)\`
|
|
493
494
|
- Shadows: \`var(--ggui-shape-shadow-sm)\`, \`var(--ggui-shape-shadow-md)\`, \`var(--ggui-shape-shadow-lg)\`
|
|
@@ -502,7 +503,7 @@ Use the FULL primary palette throughout the component — NOT only on submit but
|
|
|
502
503
|
| Element | Token | Purpose |
|
|
503
504
|
|---------|-------|---------|
|
|
504
505
|
| Section headers, hero areas, highlight strips | \`primary-50\` / \`primary-100\` | Subtle branded backgrounds |
|
|
505
|
-
|
|
|
506
|
+
| Dividers, section rules, edges of non-interactive boxes | \`primary-200\` / \`primary-300\` | Branded structure — never a control's border (rule 5) |
|
|
506
507
|
| Accent TEXT — eyebrows, taglines, links, labels, inline arrows | \`link\` (\`var(--ggui-color-link)\`) or \`<Text tone="emphasized">\` | The brand's READABLE ink, derived per mode to clear 4.5:1 — a bare \`primary-300…600\` as a text color fails tier-0 |
|
|
507
508
|
| Icon fills, badges, active indicators | \`primary-500\` / \`primary-600\` | Core accent fill |
|
|
508
509
|
| Buttons, CTAs, filled interactive elements | \`primary-600\` / \`primary-700\` | Primary actions |
|
package/src/check/run-tier0.ts
CHANGED
|
@@ -571,6 +571,50 @@ function detectComponentNeverReturns(
|
|
|
571
571
|
* Wraps the same logic as runSelfChecks (adapters/tools.ts) but emits EvalIssue[].
|
|
572
572
|
* Also runs contract validation when contract are provided.
|
|
573
573
|
*/
|
|
574
|
+
/** The kit controls whose boundary the design system draws (ggui#1697). */
|
|
575
|
+
const KIT_CONTROLS = ['Button', 'Input', 'Select', 'TextArea', 'Checkbox', 'RadioGroup', 'Toggle', 'SearchField'] as const;
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Opening tags of kit controls, read brace- and quote-aware: an attribute like `onClick={() => x}` holds a `>` that a
|
|
579
|
+
* plain `[^>]*` pattern stops at, which would hide every attribute after it.
|
|
580
|
+
*/
|
|
581
|
+
function kitControlTags(source: string): { name: string; attrs: string; line: number }[] {
|
|
582
|
+
const out: { name: string; attrs: string; line: number }[] = [];
|
|
583
|
+
const open = new RegExp(`<(${KIT_CONTROLS.join('|')})\\b`, 'g');
|
|
584
|
+
for (const m of source.matchAll(open)) {
|
|
585
|
+
const start = (m.index ?? 0) + m[0].length;
|
|
586
|
+
let depth = 0;
|
|
587
|
+
let quote: string | null = null;
|
|
588
|
+
let i = start;
|
|
589
|
+
for (; i < source.length; i += 1) {
|
|
590
|
+
const c = source[i];
|
|
591
|
+
if (quote !== null) {
|
|
592
|
+
if (c === quote && source[i - 1] !== '\\') quote = null;
|
|
593
|
+
} else if (c === '"' || c === "'" || c === '`') quote = c;
|
|
594
|
+
else if (c === '{') depth += 1;
|
|
595
|
+
else if (c === '}') depth -= 1;
|
|
596
|
+
else if (c === '>' && depth === 0) break;
|
|
597
|
+
}
|
|
598
|
+
out.push({ name: m[1] ?? '', attrs: source.slice(start, i), line: source.slice(0, m.index ?? 0).split('\n').length });
|
|
599
|
+
}
|
|
600
|
+
return out;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/** A border key (not a radius) in the tag's inline style, or in a same-file object its `style={name}` points at. */
|
|
604
|
+
function inlineBorderKey(attrs: string, source: string): string | null {
|
|
605
|
+
const BORDER = /\b(border(?:Top|Right|Bottom|Left)?(?:Color|Width|Style)?)\s*:/;
|
|
606
|
+
const style = attrs.match(/\bstyle=\{\{([\s\S]*)\}\}/);
|
|
607
|
+
if (style !== null) {
|
|
608
|
+
const hit = (style[1] ?? '').match(BORDER);
|
|
609
|
+
if (hit !== null) return hit[1] ?? null;
|
|
610
|
+
}
|
|
611
|
+
const named = attrs.match(/\bstyle=\{\s*([A-Za-z_$][\w$]*)\s*\}/);
|
|
612
|
+
if (named === null) return null;
|
|
613
|
+
const decl = source.match(new RegExp(`\\b(?:const|let)\\s+${named[1]}\\b[^=]*=\\s*\\{([^{}]*)\\}`));
|
|
614
|
+
const hit = decl !== null ? (decl[1] ?? '').match(BORDER) : null;
|
|
615
|
+
return hit !== null ? hit[1] ?? null : null;
|
|
616
|
+
}
|
|
617
|
+
|
|
574
618
|
export async function runTier0Checks(
|
|
575
619
|
sourceCode: string,
|
|
576
620
|
contract?: DataContract,
|
|
@@ -1169,6 +1213,23 @@ export async function runTier0Checks(
|
|
|
1169
1213
|
}
|
|
1170
1214
|
}
|
|
1171
1215
|
|
|
1216
|
+
// ── A kit control's boundary is the kit's (ggui#1697) ──────
|
|
1217
|
+
// A control draws its own edge, derived to clear 3:1 on its surface (`controlOutline`, or
|
|
1218
|
+
// `controlAccentOutline` on an outline Button); an inline border paints over it, and the ramp step
|
|
1219
|
+
// the model reached for (`primary-200`) measured about 1.1:1. The prompt's rule 5 says the same, so
|
|
1220
|
+
// this check enforces what the model was told. The description starts with the check's id so the
|
|
1221
|
+
// generator's logged violation lines (`✗ …`) count its fires per generation, in-turn repairs included.
|
|
1222
|
+
for (const tag of enforceDesignVocabulary ? kitControlTags(sourceCode) : []) {
|
|
1223
|
+
const key = inlineBorderKey(tag.attrs, sourceCode);
|
|
1224
|
+
if (key === null) continue;
|
|
1225
|
+
issues.push({
|
|
1226
|
+
tier: 0, result: 'fail', category: 'tokens', subcategory: 'control-inline-border', severity: 'critical',
|
|
1227
|
+
description: `control-inline-border: <${tag.name}> sets an inline \`${key}\` — a kit control draws its own boundary, and an inline border paints over it`,
|
|
1228
|
+
fix: `Remove the border from <${tag.name}>'s style (keep borderRadius if you need it). An outline chip is <Button variant="outline"> exactly as it comes; a control-like edge on your own element uses var(--ggui-color-controlOutline).`,
|
|
1229
|
+
line: tag.line,
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1172
1233
|
// ── Missing Props interface ───────────────────────────────
|
|
1173
1234
|
if (!sourceCode.includes('interface Props') && !sourceCode.includes('type Props')) {
|
|
1174
1235
|
issues.push({
|
|
@@ -49,6 +49,7 @@ These tokens adapt automatically to any theme (light, dark, branded). **ALWAYS u
|
|
|
49
49
|
| link | var(--ggui-color-link) | Accent TEXT — eyebrows, taglines, links, labels, inline arrows: the brand's readable ink, derived per mode to clear 4.5:1 on the container and the ground |
|
|
50
50
|
| outline | var(--ggui-color-outline) | Borders, dividers |
|
|
51
51
|
| outlineVariant | var(--ggui-color-outlineVariant) | Subtle borders |
|
|
52
|
+
| controlOutline | var(--ggui-color-controlOutline) | A control's edge — clears 3:1 on its surface; kit controls draw it themselves |
|
|
52
53
|
|
|
53
54
|
**Usage pattern:**
|
|
54
55
|
- Page/section background → \`var(--ggui-color-ground)\`
|
|
@@ -173,7 +174,7 @@ Use \`var(--ggui-spacing-N)\` for all padding, gap, and margin values. **Never u
|
|
|
173
174
|
### Color usage guide
|
|
174
175
|
- **Layering roles** (ground, container, elevated, sunken + their on* inks; outline): one kind of AREA each — use them for every background/text/border decision; they adapt to any theme
|
|
175
176
|
- **primary-50/100**: Section backgrounds, highlight strips, card headers
|
|
176
|
-
- **primary-200/300**:
|
|
177
|
+
- **primary-200/300**: Dividers and section rules — never a control's border: kit controls draw their own edge (\`controlOutline\` / \`controlAccentOutline\`), so set no inline border on them
|
|
177
178
|
- **primary-500/600**: Filled buttons, CTAs, badges, icon fills, active indicators — fills and borders, NOT a text color
|
|
178
179
|
- **link**: Accent TEXT — eyebrows, taglines, links, labels, inline arrows; the brand's readable ink per mode (a bare \`primary-300…600\` as a text color fails tier-0: \`universal.accent_text_ink\`)
|
|
179
180
|
- **primary-700/800/900**: Headings and text on light primary backgrounds
|
|
@@ -30,6 +30,16 @@ export const INSTRUMENT_BESIDE_JUDGE: Readonly<Record<string, 'edge'>> = {
|
|
|
30
30
|
'space.edge': 'edge',
|
|
31
31
|
};
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* ggui#1711 — judge rows a single pre-action frame cannot answer. Their own text makes them not-evaluated "when the
|
|
35
|
+
* capture has no post-action state", and every judged frame today is one capture taken before any action. Asked
|
|
36
|
+
* anyway, the judge answered `n/a` 94 of 94 times and left the row out 15 more times (a dropped row reads like a
|
|
37
|
+
* skip). So the absence is decided here, from the capture: such a row is never asked without a post-action capture,
|
|
38
|
+
* and it reads `n/a` with its reason, still in the tally. With one, the judge is asked as for any row.
|
|
39
|
+
*/
|
|
40
|
+
export const REQUIRES_POST_ACTION_CAPTURE: ReadonlySet<string> = new Set(['state.feedback']);
|
|
41
|
+
const NO_POST_ACTION = 'not evaluated: no post-action capture';
|
|
42
|
+
|
|
33
43
|
export interface CriteriaMeasurements {
|
|
34
44
|
readonly overflow: boolean;
|
|
35
45
|
readonly contentHeight: number | null;
|
|
@@ -40,6 +50,8 @@ export interface CriteriaMeasurements {
|
|
|
40
50
|
* `null` when the probe ran and failed.
|
|
41
51
|
*/
|
|
42
52
|
readonly edge?: EdgeVerdict | null;
|
|
53
|
+
/** ggui#1711 — the capture includes a state after an action ran. Absent = no (every capture today). */
|
|
54
|
+
readonly postActionCapture?: boolean;
|
|
43
55
|
}
|
|
44
56
|
|
|
45
57
|
function besideRead(kind: 'edge', m: CriteriaMeasurements): Read {
|
|
@@ -73,6 +85,7 @@ function instrumentRead(kind: 'fit' | 'fill', m: CriteriaMeasurements): Read {
|
|
|
73
85
|
}
|
|
74
86
|
|
|
75
87
|
function readRow(row: BankRow, answers: readonly CriteriaAnswer[][], m: CriteriaMeasurements, unansweredReason: string | undefined): Read {
|
|
88
|
+
if (row.evaluation === 'judge' && REQUIRES_POST_ACTION_CAPTURE.has(row.id) && m.postActionCapture !== true) return { verdict: 'n/a', evidence: NO_POST_ACTION };
|
|
76
89
|
if (row.evaluation === 'judge') return majority(row.id, answers, unansweredReason);
|
|
77
90
|
if (row.evaluation === 'human') return { verdict: 'n/a', evidence: `reader's column (status ${row.status})` };
|
|
78
91
|
if (row.status !== 'live') return { verdict: 'n/a', evidence: `not implemented (status ${row.status})` };
|
|
@@ -147,6 +160,8 @@ export interface CriteriaJudgeInputs {
|
|
|
147
160
|
readonly frame?: CriteriaJudgeFrame;
|
|
148
161
|
/** JSON text of the props the card was rendered with; bounded by {@link CRITERIA_PROPS_MAX_CHARS}. */
|
|
149
162
|
readonly propsJson?: string;
|
|
163
|
+
/** ggui#1711 — the capture includes a post-action state; absent = no, and rows needing one are not asked. */
|
|
164
|
+
readonly postActionCapture?: boolean;
|
|
150
165
|
}
|
|
151
166
|
|
|
152
167
|
/** Bound on the props text handed to the judge beside the frame, in characters. */
|
|
@@ -190,7 +205,10 @@ function criteriaPropsSection(propsJson: string): string {
|
|
|
190
205
|
/** The judge's prompt block for the selected `judge` rows; empty when none is selected. */
|
|
191
206
|
export function buildCriteriaJudgeBlock(bank: CriteriaBank, selected: CriteriaSelectionResult, inputs: CriteriaJudgeInputs = {}): string {
|
|
192
207
|
const byId = new Map<string, BankRow>(bankRows(bank).map((r) => [r.id, r]));
|
|
193
|
-
const asked = selected.selection
|
|
208
|
+
const asked = selected.selection
|
|
209
|
+
.map((s) => byId.get(s.id))
|
|
210
|
+
.filter((r): r is BankRow => r !== undefined && r.evaluation === 'judge')
|
|
211
|
+
.filter((r) => inputs.postActionCapture === true || !REQUIRES_POST_ACTION_CAPTURE.has(r.id));
|
|
194
212
|
if (asked.length === 0) return '';
|
|
195
213
|
const lines = asked.map((r) => `- ${r.id} (${r.severity}): ${r.text} Evidence must name: ${r.evidence}`);
|
|
196
214
|
return (
|
package/src/evaluation/loop.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// packages/ui-gen/src/evaluation/loop.ts
|
|
2
2
|
|
|
3
3
|
import { query, type McpServerConfig } from '@anthropic-ai/claude-agent-sdk';
|
|
4
|
+
import { sdkToolPins } from '../adapters/claude/sdk-tool-pins';
|
|
4
5
|
import { runEvaluation } from './evaluator';
|
|
5
6
|
import {
|
|
6
7
|
extractCompiledCodeFromMessage,
|
|
@@ -39,8 +40,13 @@ export interface EvaluationLoopOptions {
|
|
|
39
40
|
cwd?: string;
|
|
40
41
|
/** MCP servers available to the fix agent */
|
|
41
42
|
mcpServers?: Record<string, McpServerConfig>;
|
|
42
|
-
/**
|
|
43
|
+
/** Tool names to approve. Absent → every tool of every server in `mcpServers` (ggui#1714). */
|
|
43
44
|
allowedTools?: string[];
|
|
45
|
+
/**
|
|
46
|
+
* CLI built-in tools to offer the fix agent (e.g. `['Write']`). Default: none.
|
|
47
|
+
* An explicit opt-in: a built-in that writes or executes acts on the host (ggui#1714).
|
|
48
|
+
*/
|
|
49
|
+
builtinTools?: readonly string[];
|
|
44
50
|
/** LLM model for fix rounds */
|
|
45
51
|
model?: string;
|
|
46
52
|
/** Environment variables (includes BYOK credentials) */
|
|
@@ -90,6 +96,18 @@ export async function runEvaluationLoop(
|
|
|
90
96
|
let round = 0;
|
|
91
97
|
const maxRounds = Math.min(config.maxRounds ?? 3, MAX_EVAL_ROUNDS_HARD_LIMIT);
|
|
92
98
|
|
|
99
|
+
// ggui#1714 — a fix round runs with no CLI built-ins unless asked for. With no
|
|
100
|
+
// MCP server and no built-in either, the fix agent has nothing that can return
|
|
101
|
+
// code, and every fix round would silently hand back the original: refuse.
|
|
102
|
+
const offersATool =
|
|
103
|
+
Object.keys(generatorOptions?.mcpServers ?? {}).length > 0 || (generatorOptions?.builtinTools?.length ?? 0) > 0;
|
|
104
|
+
if (maxRounds > 1 && !offersATool) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
'runEvaluationLoop: a fix round needs a tool that can return code — pass generatorOptions.mcpServers ' +
|
|
107
|
+
"(e.g. a server with compile_component) or opt into a built-in with generatorOptions.builtinTools (e.g. ['Write']).",
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
93
111
|
while (round < maxRounds) {
|
|
94
112
|
round++;
|
|
95
113
|
|
|
@@ -145,12 +163,15 @@ export async function runEvaluationLoop(
|
|
|
145
163
|
maxTurns: 15,
|
|
146
164
|
maxBudgetUsd: config.maxBudgetPerFix,
|
|
147
165
|
env,
|
|
148
|
-
permissionMode: 'bypassPermissions',
|
|
149
|
-
allowDangerouslySkipPermissions: true,
|
|
150
166
|
...(cliPath && { pathToClaudeCodeExecutable: cliPath }),
|
|
151
167
|
...(generatorOptions?.cwd && { cwd: generatorOptions.cwd }),
|
|
152
168
|
...(generatorOptions?.mcpServers && { mcpServers: generatorOptions.mcpServers }),
|
|
153
|
-
|
|
169
|
+
// ggui#1714 — no CLI built-ins unless asked for, no host settings, no permission bypass.
|
|
170
|
+
...sdkToolPins({
|
|
171
|
+
mcpServers: generatorOptions?.mcpServers,
|
|
172
|
+
allowedTools: generatorOptions?.allowedTools,
|
|
173
|
+
builtinTools: generatorOptions?.builtinTools,
|
|
174
|
+
}),
|
|
154
175
|
...(generatorOptions?.model && { model: generatorOptions.model }),
|
|
155
176
|
...(generatorOptions?.stderr && { stderr: generatorOptions.stderr }),
|
|
156
177
|
},
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import type { EvaluationResult } from './types';
|
|
13
|
+
import { sourceCodeFromToolUse } from '../adapters/claude/source-capture';
|
|
13
14
|
|
|
14
15
|
// ── SDK message type aliases ────────────────────────────────────────
|
|
15
16
|
|
|
@@ -137,13 +138,14 @@ export function extractCompiledCodeFromMessage(message: SdkMessage): string | un
|
|
|
137
138
|
// ── sourceCode extraction ───────────────────────────────────────────
|
|
138
139
|
|
|
139
140
|
/**
|
|
140
|
-
* Extract source code from an assistant's
|
|
141
|
+
* Extract source code from an assistant message's tool_use blocks.
|
|
141
142
|
*
|
|
142
|
-
*
|
|
143
|
-
* the `input.content
|
|
144
|
-
*
|
|
143
|
+
* Reads the bridged compile tool (`*__compile_component`, `input.code`)
|
|
144
|
+
* and the CLI's `Write` (`input.content`, offered only by opt-in), by the
|
|
145
|
+
* one rule in `adapters/claude/source-capture.ts` (ggui#1714). If several
|
|
146
|
+
* calls carry source, returns the last one (the final version).
|
|
145
147
|
*
|
|
146
|
-
* Used by: loop.ts
|
|
148
|
+
* Used by: loop.ts
|
|
147
149
|
*/
|
|
148
150
|
export function extractSourceCodeFromMessage(message: SdkMessage): string | undefined {
|
|
149
151
|
if ((message.type as string) !== 'assistant') return undefined;
|
|
@@ -155,12 +157,9 @@ export function extractSourceCodeFromMessage(message: SdkMessage): string | unde
|
|
|
155
157
|
if (!Array.isArray(content)) return undefined;
|
|
156
158
|
|
|
157
159
|
for (const block of content) {
|
|
158
|
-
if (block.type
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
sourceCode = input.content;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
160
|
+
if (block.type !== 'tool_use') continue;
|
|
161
|
+
const value = sourceCodeFromToolUse(block.name, block.input);
|
|
162
|
+
if (value !== undefined) sourceCode = value;
|
|
164
163
|
}
|
|
165
164
|
|
|
166
165
|
return sourceCode;
|
|
@@ -183,7 +182,7 @@ export function extractCompiledCode(messages: SdkMessage[]): string | undefined
|
|
|
183
182
|
}
|
|
184
183
|
|
|
185
184
|
/**
|
|
186
|
-
* Scan all messages for the last sourceCode
|
|
185
|
+
* Scan all messages for the last sourceCode a tool_use carried.
|
|
187
186
|
*
|
|
188
187
|
* Convenience wrapper for tests and one-shot extraction.
|
|
189
188
|
*/
|
|
@@ -376,6 +376,10 @@ export interface CanvasJudgeRecord {
|
|
|
376
376
|
/**
|
|
377
377
|
* ggui#1687 — the model asked for, and every model the provider said answered this canvas's calls (scoring and
|
|
378
378
|
* criteria), deduplicated. `served` is empty when no provider named one: unknown, never "the requested model".
|
|
379
|
+
* When the request names an alias and the provider echoes the alias (measured on a hosted route, 125 of 125
|
|
380
|
+
* canvases), `served` confirms the alias was honoured but cannot tell two snapshots behind it apart. A judge that
|
|
381
|
+
* must is pinned to a dated snapshot id instead. With the alias, a snapshot change shows only as a step on a date in
|
|
382
|
+
* the judge's pooled within-call spread, read by week. That is a read someone runs, not a monitor.
|
|
379
383
|
*/
|
|
380
384
|
readonly models?: { readonly requested: string; readonly served: readonly string[] };
|
|
381
385
|
}
|
package/src/taught-tokens.ts
CHANGED
|
@@ -18,13 +18,12 @@ import { consumedTokenManifest } from "@ggui-ai/design/themes";
|
|
|
18
18
|
* closed-set check still admits them: a card that references one is valid wherever this release
|
|
19
19
|
* renders it.
|
|
20
20
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
21
|
+
* Empty today. The control edges (`controlOutline`, `controlAccentOutline`, ggui#1494) were held here
|
|
22
|
+
* until every serving and rollback runtime declared them; the derivation (`10a0c00f2`) is in every
|
|
23
|
+
* release from tag 17 on, so they are taught from ggui#1697, in the same change as the prompt rule that
|
|
24
|
+
* teaches a control's edge.
|
|
23
25
|
*/
|
|
24
|
-
export const NOT_YET_TAUGHT_TOKENS: ReadonlySet<string> = new Set([
|
|
25
|
-
"--ggui-color-controlAccentOutline",
|
|
26
|
-
"--ggui-color-controlOutline",
|
|
27
|
-
]);
|
|
26
|
+
export const NOT_YET_TAUGHT_TOKENS: ReadonlySet<string> = new Set<string>([]);
|
|
28
27
|
|
|
29
28
|
/** The manifest as the model is taught it. */
|
|
30
29
|
export const taughtTokenManifest: readonly string[] = consumedTokenManifest.filter(
|