@ggui-ai/ui-gen 0.25.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/README.md +2 -2
- package/dist/{adapter-Cg_rkpGq.d.ts → adapter-h4_V69LI.d.ts} +49 -1
- package/dist/adapters/base.d.ts +6 -1
- package/dist/adapters/base.js.map +1 -1
- package/dist/adapters/generation-dispatch.d.ts +8 -5
- package/dist/adapters/generation-dispatch.js +1174 -198
- package/dist/adapters/generation-dispatch.js.map +1 -1
- package/dist/adapters/index.js +59 -16
- package/dist/adapters/index.js.map +1 -1
- package/dist/advanced/index.d.ts +10 -9
- package/dist/advanced/index.js +1215 -216
- package/dist/advanced/index.js.map +1 -1
- package/dist/blueprint-validator.js +416 -92
- package/dist/blueprint-validator.js.map +1 -1
- package/dist/boilerplate.d.ts +2 -2
- package/dist/boilerplate.js +10 -5
- package/dist/boilerplate.js.map +1 -1
- package/dist/check/index.d.ts +3 -8
- package/dist/check/index.js +55 -2
- package/dist/check/index.js.map +1 -1
- package/dist/{create-ui-generator-DFnnArrv.d.ts → create-ui-generator-BrZHF0cf.d.ts} +3 -4
- package/dist/{design-mode-DLufhrNp.d.ts → design-mode-DGWN_Gkm.d.ts} +1 -1
- package/dist/{dispatch-CStl9eAw.d.ts → dispatch-B07TI30d.d.ts} +2 -2
- package/dist/evaluation/axis-checks/index.d.ts +5 -4
- package/dist/evaluation/axis-checks/index.js.map +1 -1
- package/dist/evaluation/axis-checks/registry.d.ts +4 -3
- package/dist/evaluation/index.d.ts +124 -546
- package/dist/evaluation/index.js +588 -65
- package/dist/evaluation/index.js.map +1 -1
- package/dist/evaluation/types.d.ts +17 -0
- package/dist/evaluation/types.js.map +1 -1
- package/dist/{generate-9IeNT4ZY.d.ts → generate-tSO_shQ0.d.ts} +1 -1
- package/dist/harness/check/runtime-render/index.d.ts +5 -5
- package/dist/harness/check/runtime-render/index.js +416 -92
- package/dist/harness/check/runtime-render/index.js.map +1 -1
- package/dist/harness/check/runtime-render/render-check-worker.js +393 -88
- package/dist/harness/check/runtime-render/render-check-worker.js.map +1 -1
- package/dist/harness/index.d.ts +4 -5
- package/dist/harness/index.js +36 -7
- package/dist/harness/index.js.map +1 -1
- package/dist/harness/prompts.d.ts +2 -2
- package/dist/harness/prompts.js +7 -1
- package/dist/harness/prompts.js.map +1 -1
- package/dist/harness/result-types.d.ts +17 -3
- package/dist/harness/result-types.js.map +1 -1
- package/dist/harness/runtime.d.ts +14 -6
- package/dist/harness/runtime.js +13 -7
- package/dist/harness/runtime.js.map +1 -1
- package/dist/harness/types-public.d.ts +4 -2
- package/dist/harness/types-public.js +10 -5
- package/dist/harness/types-public.js.map +1 -1
- package/dist/harness/validator-trace-sink.d.ts +4 -3
- package/dist/index.d.ts +46 -11
- package/dist/index.js +1217 -214
- package/dist/index.js.map +1 -1
- package/dist/patch.d.ts +3 -3
- package/dist/{prompts-YU9piOgs.d.ts → prompts-BhNUuRoZ.d.ts} +1 -1
- package/dist/tools/render-check-worker.js +8 -1
- package/dist/tools/render-check-worker.js.map +1 -1
- package/dist/{types-public-BJTLpzWn.d.ts → types-public-h8IMnYuh.d.ts} +388 -3
- package/dist/validation/index.d.ts +1 -1
- package/dist/validation/index.js +1 -1
- package/dist/validation/index.js.map +1 -1
- package/dist/visual-evaluator-C2strxqD.d.ts +639 -0
- package/dist/workflows.d.ts +3 -3
- 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/adapters/generation-dispatch.ts +2 -0
- package/src/boilerplate/free-design-prompt.ts +3 -3
- package/src/boilerplate/hard-sections.ts +1 -1
- package/src/boilerplate/system-prompt.ts +3 -2
- package/src/boilerplate/templates/layouts/chat-mobile.tsx.tmpl +3 -1
- package/src/boilerplate/templates/layouts/chat-universal.tsx.tmpl +3 -1
- package/src/boilerplate/templates/layouts/fullscreen-desktop.tsx.tmpl +3 -1
- package/src/boilerplate/templates/layouts/fullscreen-universal.tsx.tmpl +3 -1
- package/src/check/run-tier0.ts +67 -3
- package/src/contract-context.ts +1 -1
- package/src/design-system-docs.ts +2 -1
- package/src/evaluation/cost-tracker.ts +54 -6
- package/src/evaluation/criteria/edge-probe.ts +286 -0
- package/src/evaluation/criteria/edge.ts +202 -0
- package/src/evaluation/criteria/resolve.ts +74 -8
- package/src/evaluation/index.ts +5 -1
- package/src/evaluation/judge-spend.ts +48 -0
- package/src/evaluation/loop.ts +25 -4
- package/src/evaluation/message-parsing.ts +11 -12
- package/src/evaluation/types-public.ts +95 -0
- package/src/evaluation/types.ts +10 -0
- package/src/evaluation/visual-evaluator.ts +236 -40
- package/src/harness/check/runtime-render/adapter.ts +18 -0
- package/src/harness/check/runtime-render/probe-pending.ts +111 -0
- package/src/harness/check/runtime-render/render-check-host.ts +10 -6
- package/src/harness/check/runtime-render/render-check-worker.ts +6 -3
- package/src/harness/check/runtime-render/render-check.ts +417 -133
- package/src/harness/check/runtime-render/walk.ts +200 -0
- package/src/harness/coding/assemble-result.ts +2 -0
- package/src/harness/coding/generate-task-runner.ts +27 -0
- package/src/harness/coding/init-session.ts +5 -3
- package/src/harness/coding/run-coding-turn.ts +5 -1
- package/src/harness/coding/run-eval-round.ts +63 -3
- package/src/harness/in-loop-evaluation.ts +39 -0
- package/src/harness/llm-router.ts +58 -1
- package/src/harness/result-types.ts +9 -0
- package/src/harness/runtime.ts +9 -0
- package/src/harness/types-public.ts +10 -1
- package/src/index.ts +5 -0
- package/src/internal/verdict-line.ts +33 -0
- package/src/taught-tokens.ts +31 -0
- package/src/tools/render-check-worker.ts +2 -1
- package/src/tools/render-check.ts +8 -7
- package/src/validation/primitives.ts +1 -1
- package/dist/llm-router-nfO7wPec.d.ts +0 -267
- package/dist/{bank-CT4eEkta.d.ts → bank-Ctdp3FUA.d.ts} +3 -3
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/
|
|
193
|
-
"@ggui-ai/
|
|
194
|
-
"@ggui-ai/protocol": "0.
|
|
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/gadgets": "0.
|
|
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/mcp-server-core": "0.
|
|
256
|
-
"@ggui-ai/project-config": "0.
|
|
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
|
+
}
|
|
@@ -126,6 +126,8 @@ export interface GenerationDispatchParams {
|
|
|
126
126
|
/** ggui#1436 — see `SingleComponentParams.visualEvaluation.criteriaBank`. */
|
|
127
127
|
criteriaBank?: CriteriaBank;
|
|
128
128
|
criteriaKind?: string;
|
|
129
|
+
/** ggui#1542 — see `SingleComponentParams.visualEvaluation.actOn`. */
|
|
130
|
+
actOn?: "critical" | "major";
|
|
129
131
|
};
|
|
130
132
|
/** Quality config controlling eval tiers and improvement behavior */
|
|
131
133
|
qualityConfig?: QualityConfig;
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
// for raw elements · the one-gesture-surface invariant restated for raw
|
|
19
19
|
// elements · a regenerated quality checklist.
|
|
20
20
|
|
|
21
|
-
import {
|
|
21
|
+
import { taughtTokenManifest } from "../taught-tokens.js";
|
|
22
22
|
import { describeCanvas, type CanvasClass } from "../design-mode.js";
|
|
23
23
|
import {
|
|
24
24
|
ANTI_PATTERNS,
|
|
@@ -136,7 +136,7 @@ function renderRamp(r: ColorRamp): string {
|
|
|
136
136
|
* every role the prose names exists on `:root`; nothing is implied by a
|
|
137
137
|
* `50…800` range that the manifest does not carry step for step.
|
|
138
138
|
*/
|
|
139
|
-
export function renderFreeColorRule(manifest: readonly string[] =
|
|
139
|
+
export function renderFreeColorRule(manifest: readonly string[] = taughtTokenManifest): string {
|
|
140
140
|
const { ramps, roles } = colorVocabularyFromManifest(manifest);
|
|
141
141
|
const brand = ramps.filter((r) => r.family === "primary" || r.family === "neutral");
|
|
142
142
|
const state = ramps.filter((r) => r.family !== "primary" && r.family !== "neutral");
|
|
@@ -260,7 +260,7 @@ const TOKEN_GROUPS: readonly TokenGroup[] = [
|
|
|
260
260
|
* does not define. Grouped by prefix; anything outside the known
|
|
261
261
|
* prefixes lands under "Other".
|
|
262
262
|
*/
|
|
263
|
-
export function renderTokenVocabulary(manifest: readonly string[] =
|
|
263
|
+
export function renderTokenVocabulary(manifest: readonly string[] = taughtTokenManifest): string {
|
|
264
264
|
const sorted = [...manifest].sort();
|
|
265
265
|
const claimed = new Set<string>();
|
|
266
266
|
const lines: string[] = [
|
|
@@ -257,7 +257,7 @@ The width works the same way, and the mistake is the mirror image: **your outerm
|
|
|
257
257
|
|
|
258
258
|
- Never put \`maxWidth\` (or a \`max-w-*\` class) on the element you return at the top. On a wide canvas that turns your card into a narrow strip with the frame's ground on either side of it, and on a narrow one it does nothing — the same composition reading differently at two sizes.
|
|
259
259
|
- DO cap the reading measure inside: a column of prose is easiest to read at 60–70 characters, so wrap the TEXT in a \`<Container maxWidth="sm">\` (or your own \`max-width\`) and leave the root filling. A hero band, a toolbar, a table or an image then still uses the whole width, which is what makes a wide card look composed rather than padded.
|
|
260
|
-
- Centre that inner column with the container's own centring, not by shrinking the root.
|
|
260
|
+
- Centre that inner column with the container's own centring, not by shrinking the root — and give the card ONE column: every block of the reading flow (a heading, its text, and the chips or buttons that answer it) sits in a \`<Container>\` of the same \`maxWidth\`, so their left edges line up even where a full-width band runs between them.
|
|
261
261
|
- **They are two different elements.** Even when the whole card is one column of text, the root still fills and the column sits INSIDE it — \`<Box padding="lg"><Container maxWidth="sm">…</Container></Box>\`, never a root that is itself the container. A centred card is centring inside a filled frame, not a shrunken frame.`;
|
|
262
262
|
|
|
263
263
|
/** Helper-component structure guidance (JSX depth 3–5, helpers above `Component`). Identical in both modes. */
|
|
@@ -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 |
|
|
@@ -3,5 +3,7 @@
|
|
|
3
3
|
provides the chrome, so add no shadow or outer radius of your own.
|
|
4
4
|
Compact and natural height; stack vertically. This root fills the
|
|
5
5
|
bubble — wrap PROSE in <Container maxWidth="sm"> to cap the reading
|
|
6
|
-
measure (inert at bubble width, composed on a wider frame)
|
|
6
|
+
measure (inert at bubble width, composed on a wider frame), and put
|
|
7
|
+
the chips or buttons that answer it in a Container of the same
|
|
8
|
+
maxWidth, so every block keeps one left edge. */}
|
|
7
9
|
</Box>
|
|
@@ -3,5 +3,7 @@
|
|
|
3
3
|
provides the chrome, so add no shadow or outer radius of your own.
|
|
4
4
|
Compact and natural height; stack vertically. This root fills the
|
|
5
5
|
bubble — wrap PROSE in <Container maxWidth="sm"> to cap the reading
|
|
6
|
-
measure (inert at bubble width, composed on a wider frame)
|
|
6
|
+
measure (inert at bubble width, composed on a wider frame), and put
|
|
7
|
+
the chips or buttons that answer it in a Container of the same
|
|
8
|
+
maxWidth, so every block keeps one left edge. */}
|
|
7
9
|
</Box>
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
<Box padding="lg">
|
|
2
2
|
{/* implement your UI here — this root FILLS the frame.
|
|
3
3
|
Bands, toolbars, tables and images use the whole width;
|
|
4
|
-
wrap PROSE in <Container maxWidth="sm"> to cap the reading measure
|
|
4
|
+
wrap PROSE in <Container maxWidth="sm"> to cap the reading measure,
|
|
5
|
+
and put the chips or buttons that answer it in a Container of the
|
|
6
|
+
same maxWidth, so every block keeps one left edge. */}
|
|
5
7
|
</Box>
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
<Box padding="lg">
|
|
2
2
|
{/* implement your UI here — this root FILLS the frame.
|
|
3
3
|
Bands, toolbars, tables and images use the whole width;
|
|
4
|
-
wrap PROSE in <Container maxWidth="sm"> to cap the reading measure
|
|
4
|
+
wrap PROSE in <Container maxWidth="sm"> to cap the reading measure,
|
|
5
|
+
and put the chips or buttons that answer it in a Container of the
|
|
6
|
+
same maxWidth, so every block keeps one left edge. */}
|
|
5
7
|
</Box>
|
package/src/check/run-tier0.ts
CHANGED
|
@@ -17,6 +17,7 @@ import type { DataContract } from '@ggui-ai/protocol';
|
|
|
17
17
|
import { HOOK_NAME_RE, listContractGadgets } from '@ggui-ai/protocol';
|
|
18
18
|
import { consumedTokenManifest } from '@ggui-ai/design/themes';
|
|
19
19
|
import { DEFAULT_DESIGN_MODE, type DesignMode } from '../design-mode.js';
|
|
20
|
+
import { taughtTokenManifest } from '../taught-tokens.js';
|
|
20
21
|
import { validateAllContracts, type ContractIssue } from './contract-validation.js';
|
|
21
22
|
import { typecheck } from './type-checker.js';
|
|
22
23
|
import { lintReactHooks, type ReactLintDiagnostic } from './react-linter.js';
|
|
@@ -315,7 +316,9 @@ const RETIRED_TOKEN_SUCCESSORS: ReadonlyMap<string, string> = new Map([
|
|
|
315
316
|
* (`--ggui-motion-duration-normal` → fast, base, slow); otherwise the names
|
|
316
317
|
* sharing any of its segments of four letters or more
|
|
317
318
|
* (`--ggui-duration-slow` → the motion durations). Empty when nothing is
|
|
318
|
-
* near, so the hint falls back to the manifest's general shape.
|
|
319
|
+
* near, so the hint falls back to the manifest's general shape. A hint
|
|
320
|
+
* teaches, so it draws only on the names the model may be taught
|
|
321
|
+
* (`taughtTokenManifest`); the closed-set check above admits the full manifest.
|
|
319
322
|
*/
|
|
320
323
|
export function suggestManifestTokens(name: string): readonly string[] {
|
|
321
324
|
const successor = RETIRED_TOKEN_SUCCESSORS.get(name);
|
|
@@ -328,7 +331,7 @@ export function suggestManifestTokens(name: string): readonly string[] {
|
|
|
328
331
|
const own = name.endsWith('-') ? segments.length : segments.length - 1;
|
|
329
332
|
for (let n = own; n >= 2; n--) {
|
|
330
333
|
const prefix = `--${segments.slice(0, n).join('-')}-`;
|
|
331
|
-
const family =
|
|
334
|
+
const family = taughtTokenManifest.filter((token) => token.startsWith(prefix));
|
|
332
335
|
if (family.length > 0) return wordHitsFirst(name, rolesFirst(family));
|
|
333
336
|
}
|
|
334
337
|
const words = new Set(
|
|
@@ -338,7 +341,7 @@ export function suggestManifestTokens(name: string): readonly string[] {
|
|
|
338
341
|
.map((segment) => segment.toLowerCase()),
|
|
339
342
|
);
|
|
340
343
|
return rolesFirst(
|
|
341
|
-
|
|
344
|
+
taughtTokenManifest.filter((token) =>
|
|
342
345
|
token.split('-').some((segment) => words.has(segment.toLowerCase())),
|
|
343
346
|
),
|
|
344
347
|
);
|
|
@@ -568,6 +571,50 @@ function detectComponentNeverReturns(
|
|
|
568
571
|
* Wraps the same logic as runSelfChecks (adapters/tools.ts) but emits EvalIssue[].
|
|
569
572
|
* Also runs contract validation when contract are provided.
|
|
570
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
|
+
|
|
571
618
|
export async function runTier0Checks(
|
|
572
619
|
sourceCode: string,
|
|
573
620
|
contract?: DataContract,
|
|
@@ -1166,6 +1213,23 @@ export async function runTier0Checks(
|
|
|
1166
1213
|
}
|
|
1167
1214
|
}
|
|
1168
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
|
+
|
|
1169
1233
|
// ── Missing Props interface ───────────────────────────────
|
|
1170
1234
|
if (!sourceCode.includes('interface Props') && !sourceCode.includes('type Props')) {
|
|
1171
1235
|
issues.push({
|
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: 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.`,
|
|
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; the chips or buttons that answer the prose sit in a Container of the same maxWidth, so they share its left edge.`,
|
|
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)
|
|
@@ -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
|
|
@@ -31,15 +31,54 @@ function resolveRegistryId(model: string): ModelId | null {
|
|
|
31
31
|
return keys.find((id) => id.endsWith(`/${undated}`)) ?? null;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* ggui#1524 — the largest cache-WRITE premium any registry model states (`cacheWritePer1M / inputPer1M`), never
|
|
36
|
+
* below 1. A cost CAP errs toward over-counting, so a model that states no write rate is priced at this bound
|
|
37
|
+
* times its input rate: a write costs a premium over input wherever one is charged, and pricing it at the bare
|
|
38
|
+
* input rate would slip that premium under the cap. Derived from the registry, so it moves with it.
|
|
39
|
+
*/
|
|
40
|
+
export const CACHE_WRITE_PREMIUM_BOUND: number = Math.max(
|
|
41
|
+
1,
|
|
42
|
+
...Object.values(MODEL_REGISTRY).flatMap(({ costs }) =>
|
|
43
|
+
costs.cacheWritePer1M !== undefined && costs.inputPer1M > 0 ? [costs.cacheWritePer1M / costs.inputPer1M] : [],
|
|
44
|
+
),
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
/** Per-1K USD for each kind of token a call can spend. */
|
|
48
|
+
export interface PricePer1k {
|
|
49
|
+
readonly input: number;
|
|
50
|
+
readonly output: number;
|
|
51
|
+
/** Cache reads: the model's stated rate, else its input rate — over-counts a read, the safe side for a cap. */
|
|
52
|
+
readonly cacheRead: number;
|
|
53
|
+
/** Cache writes: the model's stated rate, else its input rate × {@link CACHE_WRITE_PREMIUM_BOUND}. */
|
|
54
|
+
readonly cacheWrite: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
34
57
|
/** Fallback when a model has no registry entry — Sonnet-class rates. */
|
|
35
|
-
const FALLBACK_PER_1K = {
|
|
58
|
+
const FALLBACK_PER_1K: PricePer1k = {
|
|
59
|
+
input: 0.003,
|
|
60
|
+
output: 0.015,
|
|
61
|
+
cacheRead: 0.003,
|
|
62
|
+
cacheWrite: 0.003 * CACHE_WRITE_PREMIUM_BOUND,
|
|
63
|
+
};
|
|
36
64
|
|
|
37
|
-
/** Per-1K
|
|
38
|
-
export function pricePer1k(model: string):
|
|
65
|
+
/** Per-1K USD for a bare wire model id: input, output, and the two cache kinds. */
|
|
66
|
+
export function pricePer1k(model: string): PricePer1k {
|
|
39
67
|
const id = resolveRegistryId(model);
|
|
40
68
|
if (id === null) return FALLBACK_PER_1K;
|
|
41
69
|
const { costs } = MODEL_REGISTRY[id];
|
|
42
|
-
return {
|
|
70
|
+
return {
|
|
71
|
+
input: costs.inputPer1M / 1000,
|
|
72
|
+
output: costs.outputPer1M / 1000,
|
|
73
|
+
cacheRead: (costs.cacheReadPer1M ?? costs.inputPer1M) / 1000,
|
|
74
|
+
cacheWrite: (costs.cacheWritePer1M ?? costs.inputPer1M * CACHE_WRITE_PREMIUM_BOUND) / 1000,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** ggui#1524 — the cache tokens a call spent, as its provider reported them; absent = unreported. */
|
|
79
|
+
export interface CacheSpend {
|
|
80
|
+
readonly read?: number;
|
|
81
|
+
readonly write?: number;
|
|
43
82
|
}
|
|
44
83
|
|
|
45
84
|
export class CostTracker {
|
|
@@ -47,9 +86,18 @@ export class CostTracker {
|
|
|
47
86
|
|
|
48
87
|
constructor(private maxBudget: number | null) {}
|
|
49
88
|
|
|
50
|
-
|
|
89
|
+
/**
|
|
90
|
+
* Record one call. `inputTokens` is the provider's UNCACHED input; a call that read or wrote the prompt cache
|
|
91
|
+
* passes those counts in `cache` (ggui#1524), priced at the cache rates — until then a turn with a long cached
|
|
92
|
+
* prefix cost the cap almost nothing while the provider billed its reads and writes.
|
|
93
|
+
*/
|
|
94
|
+
record(model: string, inputTokens: number, outputTokens: number, cache: CacheSpend = {}): number {
|
|
51
95
|
const prices = pricePer1k(model);
|
|
52
|
-
const cost =
|
|
96
|
+
const cost =
|
|
97
|
+
(inputTokens / 1000) * prices.input +
|
|
98
|
+
(outputTokens / 1000) * prices.output +
|
|
99
|
+
((cache.read ?? 0) / 1000) * prices.cacheRead +
|
|
100
|
+
((cache.write ?? 0) / 1000) * prices.cacheWrite;
|
|
53
101
|
this.totalCost += cost;
|
|
54
102
|
return cost;
|
|
55
103
|
}
|