@ggui-ai/ui-gen 0.15.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/dist/adapters/generation-dispatch.d.ts +24 -4
  2. package/dist/adapters/generation-dispatch.js +5188 -3920
  3. package/dist/adapters/generation-dispatch.js.map +1 -1
  4. package/dist/adapters/index.js +33 -26
  5. package/dist/adapters/index.js.map +1 -1
  6. package/dist/advanced/index.d.ts +22 -13
  7. package/dist/advanced/index.js +4889 -3561
  8. package/dist/advanced/index.js.map +1 -1
  9. package/dist/blueprint-validator.js +8 -12
  10. package/dist/blueprint-validator.js.map +1 -1
  11. package/dist/boilerplate.d.ts +19 -1
  12. package/dist/boilerplate.js +671 -254
  13. package/dist/boilerplate.js.map +1 -1
  14. package/dist/check/index.d.ts +17 -3
  15. package/dist/check/index.js +34 -25
  16. package/dist/check/index.js.map +1 -1
  17. package/dist/compose.js +1 -1
  18. package/dist/compose.js.map +1 -1
  19. package/dist/create-ui-generator-C8Ki9Wz1.d.ts +189 -0
  20. package/dist/design-mode-DLufhrNp.d.ts +61 -0
  21. package/dist/{dispatch-BuZfTh9r.d.ts → dispatch-D2htzi9h.d.ts} +4 -1
  22. package/dist/evaluation/axis-checks/index.d.ts +4 -3
  23. package/dist/evaluation/axis-checks/index.js +300 -14
  24. package/dist/evaluation/axis-checks/index.js.map +1 -1
  25. package/dist/evaluation/axis-checks/registry.d.ts +2 -1
  26. package/dist/evaluation/axis-checks/registry.js +253 -1
  27. package/dist/evaluation/axis-checks/registry.js.map +1 -1
  28. package/dist/evaluation/index.d.ts +297 -5
  29. package/dist/evaluation/index.js +4515 -120
  30. package/dist/evaluation/index.js.map +1 -1
  31. package/dist/fragments/index.js +1 -1
  32. package/dist/fragments/index.js.map +1 -1
  33. package/dist/{generate-BTfvELgl.d.ts → generate-9IeNT4ZY.d.ts} +10 -1
  34. package/dist/harness/check/runtime-render/index.d.ts +3 -1
  35. package/dist/harness/check/runtime-render/index.js +8 -12
  36. package/dist/harness/check/runtime-render/index.js.map +1 -1
  37. package/dist/harness/index.d.ts +3 -1
  38. package/dist/harness/index.js +913 -441
  39. package/dist/harness/index.js.map +1 -1
  40. package/dist/harness/llm-trace-sink.d.ts +3 -3
  41. package/dist/harness/llm-trace-sink.js.map +1 -1
  42. package/dist/harness/prompts.d.ts +2 -1
  43. package/dist/harness/prompts.js +6 -3
  44. package/dist/harness/prompts.js.map +1 -1
  45. package/dist/harness/result-types.d.ts +17 -5
  46. package/dist/harness/result-types.js.map +1 -1
  47. package/dist/harness/runtime.d.ts +28 -3
  48. package/dist/harness/runtime.js +702 -274
  49. package/dist/harness/runtime.js.map +1 -1
  50. package/dist/harness/types-public.d.ts +39 -1
  51. package/dist/harness/types-public.js +913 -441
  52. package/dist/harness/types-public.js.map +1 -1
  53. package/dist/harness/validator-trace-sink.d.ts +2 -1
  54. package/dist/index.d.ts +32 -6
  55. package/dist/index.js +4899 -3560
  56. package/dist/index.js.map +1 -1
  57. package/dist/patch.d.ts +3 -1
  58. package/dist/{prompts-BVhtH3M1.d.ts → prompts-YU9piOgs.d.ts} +21 -45
  59. package/dist/providers/index.d.ts +2 -2
  60. package/dist/providers/index.js.map +1 -1
  61. package/dist/styling-profile-CxfvV48g.d.ts +35 -0
  62. package/dist/tools/get-primitives-ts.d.ts +1 -1
  63. package/dist/tools/get-primitives-ts.js +1 -1
  64. package/dist/tools/get-primitives-ts.js.map +1 -1
  65. package/dist/{types-public-CkNWEov9.d.ts → types-public-DVSssdc-.d.ts} +85 -5
  66. package/dist/validation/index.d.ts +1 -1
  67. package/dist/validation/index.js +4 -4
  68. package/dist/validation/index.js.map +1 -1
  69. package/dist/workflows.d.ts +3 -1
  70. package/package.json +12 -12
  71. package/src/adapters/generation-dispatch.ts +62 -4
  72. package/src/adapters/provider-router.ts +7 -6
  73. package/src/adapters/tools.ts +5 -5
  74. package/src/advanced/generator.ts +11 -10
  75. package/src/advanced/index.ts +5 -4
  76. package/src/boilerplate/free-design-prompt.ts +336 -0
  77. package/src/boilerplate/generate.ts +10 -1
  78. package/src/boilerplate/hard-sections.ts +257 -0
  79. package/src/boilerplate/render.ts +32 -8
  80. package/src/boilerplate/styling-profile.ts +166 -0
  81. package/src/boilerplate/system-prompt.ts +101 -237
  82. package/src/boilerplate/templates/base-free.tsx.tmpl +24 -0
  83. package/src/check/run-tier0.ts +82 -20
  84. package/src/coding-agent/tools.ts +8 -2
  85. package/src/contract-context.ts +43 -14
  86. package/src/create-harness.ts +15 -0
  87. package/src/create-ui-generator.ts +112 -27
  88. package/src/design-mode.ts +192 -0
  89. package/src/design-system-docs.ts +24 -17
  90. package/src/effort.ts +48 -0
  91. package/src/evaluation/axis-checks/checks/grid.ts +148 -0
  92. package/src/evaluation/axis-checks/checks/universal.ts +244 -0
  93. package/src/evaluation/axis-checks/dispatch.ts +82 -13
  94. package/src/evaluation/axis-checks/extras.ts +5 -0
  95. package/src/evaluation/axis-checks/helpers.ts +12 -0
  96. package/src/evaluation/axis-checks/registry.ts +2 -0
  97. package/src/evaluation/cost-tracker.ts +2 -3
  98. package/src/evaluation/design-identity.ts +96 -0
  99. package/src/evaluation/index.ts +63 -2
  100. package/src/evaluation/llm-evaluator.ts +89 -21
  101. package/src/evaluation/theme-css.ts +14 -0
  102. package/src/evaluation/types-public.ts +137 -8
  103. package/src/evaluation/visual-evaluator.ts +759 -54
  104. package/src/fragments/render.ts +1 -1
  105. package/src/harness/check/runtime-render/prepare-mockup.ts +18 -11
  106. package/src/harness/coding/dupe-break.ts +0 -0
  107. package/src/harness/coding/init-session.ts +2 -0
  108. package/src/harness/coding/run-coding-turn.ts +9 -14
  109. package/src/harness/coding/run-eval-round.ts +46 -5
  110. package/src/harness/llm-trace-sink.ts +3 -3
  111. package/src/harness/result-types.ts +15 -3
  112. package/src/harness/runtime.ts +28 -0
  113. package/src/harness/types-public.ts +38 -0
  114. package/src/index.ts +28 -0
  115. package/src/internal/extract-call-signatures.ts +2 -2
  116. package/src/pin-fixtures.ts +98 -0
  117. package/src/providers/bedrock.ts +4 -4
  118. package/src/run-check.ts +11 -10
  119. package/src/tools/get-primitives-ts.ts +1 -1
  120. package/src/tools.ts +2 -1
  121. package/src/validation/allowed-imports.ts +8 -2
  122. package/src/validation/primitives.ts +1 -1
  123. package/dist/create-ui-generator-D6TKuv9V.d.ts +0 -106
@@ -31,11 +31,15 @@
31
31
  * Callers who want a lightweight single-shot path can build it
32
32
  * themselves.
33
33
  */
34
+ import type { SingleComponentParams } from './harness/runtime.js';
34
35
  import type {
35
36
  GadgetDescriptor,
36
37
  GenerationError,
37
38
  JsonObject,
39
+ GeneratorId,
40
+ ModelRef,
38
41
  } from '@ggui-ai/protocol';
42
+ import { isGeneratorId, modelRefOfRoute } from '@ggui-ai/protocol';
39
43
  import type { GadgetCatalogAdapter } from '@ggui-ai/gadgets';
40
44
  import type {
41
45
  GenerationMetadata,
@@ -52,20 +56,26 @@ import {
52
56
  } from '@ggui-ai/mcp-server-core';
53
57
  import { createGeneratorTools } from './adapters/index.js';
54
58
  import { dispatchGeneration } from './adapters/generation-dispatch.js';
59
+ import { hasProfile } from './boilerplate/styling-profile.js';
60
+ import { effortDials } from './effort.js';
55
61
  import type { ProviderName } from './adapters/types.js';
62
+ import type { GenerationResult } from './harness/result-types.js';
56
63
  import {
64
+ canvasForRenderingContext,
57
65
  injectContracts,
58
66
  injectRenderingContext,
59
67
  injectVariance,
60
68
  } from './contract-context.js';
61
69
  import type { RenderingContext } from './contract-context.js';
70
+ import type { DesignMode } from './design-mode.js';
62
71
  import { resolveRoute, applyRouteToEnv } from './adapters/provider-router.js';
63
72
  import type { QualityConfig } from './evaluation/types-public.js';
73
+ import type { EvaluationConfig } from './evaluation/types.js';
64
74
  import type { AgentConfig } from './harness/llm-router.js';
65
75
 
66
76
  /** The slug for the OSS default seed generator. */
67
77
  const DEFAULT_TIER: GeneratorTier = 'default';
68
- const DEFAULT_MODEL = 'haiku-4-5';
78
+ const DEFAULT_MODEL: ModelRef = 'anthropic/claude-haiku-4-5';
69
79
 
70
80
  export interface CreateUiGeneratorOptions {
71
81
  /**
@@ -83,27 +93,42 @@ export interface CreateUiGeneratorOptions {
83
93
  readonly maxAttempts?: number;
84
94
  /** Maximum evaluation rounds. */
85
95
  readonly maxEvalRounds?: number;
96
+ /**
97
+ * In-loop LLM evaluation config (ggui#1001). When set, every generation
98
+ * runs the evaluator tiers after the coding leg (`maxRounds` defaults to
99
+ * `maxEvalRounds`) and the harness result carries `evalResult` — the
100
+ * probe verdict a downstream judge reads. Default: none, as today —
101
+ * a serving deployment leaves it unset; offline minting jobs set a round.
102
+ */
103
+ readonly evaluation?: EvaluationConfig;
104
+ /**
105
+ * In-loop visual round config (per-canvas judging, a sample, the
106
+ * stylesheet to render under). Forwarded verbatim; never part of the
107
+ * prompt or the identity. Default: none — the round runs as today.
108
+ */
109
+ readonly visualEvaluation?: SingleComponentParams['visualEvaluation'];
86
110
  /** Quality config controlling eval tiers + improvement behavior. */
87
111
  readonly qualityConfig?: QualityConfig;
88
112
  /**
89
- * Generator identity — registry key + parsed components.
113
+ * Generator identity — registry key + declared model.
90
114
  *
91
- * The tier + model determine the {@link UiGenerator.slug} the
92
- * factory bakes onto the returned generator. Defaults to
93
- * `{tier: 'default', model: 'haiku-4-5'}` → slug
94
- * `ui-gen-default-haiku-4-5`, the OSS seed.
115
+ * The tier alone determines the {@link UiGenerator.slug} the factory
116
+ * bakes onto the returned generator (`ui-gen-<tier>`); `model` is the
117
+ * generator's declared model field and is never part of the slug
118
+ * (ggui#923). Defaults to tier `'default'` → slug `ui-gen-default`,
119
+ * the OSS seed, with the factory's default model.
95
120
  *
96
121
  * The actual model used per request still comes from
97
122
  * `UiGenerateInput.llm.model` (operators may override via BYOK).
98
123
  * The identity here is the registry-level handle, not a runtime
99
124
  * model constraint.
100
125
  *
101
- * Mutually-exclusive shortcut: pass `slug` instead and the factory
102
- * parses it for you. The slug + tier/model overloads conflict; the
103
- * factory throws if both are supplied.
126
+ * Mutually-exclusive shortcut: pass `slug` instead of `tier` and the
127
+ * factory parses the tier from it; `model` may accompany either form.
128
+ * The factory throws if `slug` and `tier` are both supplied.
104
129
  */
105
130
  readonly tier?: GeneratorTier;
106
- readonly model?: string;
131
+ readonly model?: ModelRef;
107
132
  readonly slug?: string;
108
133
  /**
109
134
  * Per-deployment gadget descriptor source. Wired once at factory
@@ -163,6 +188,35 @@ export interface CreateUiGeneratorOptions {
163
188
  * still log via `console.warn`, just without a caller-side hook.
164
189
  */
165
190
  readonly onRetry?: AgentConfig['onRetry'];
191
+ /**
192
+ * Generation observer. Invoked once per successful `generate()` with
193
+ * the FULL harness result — source, compiled code, token counters,
194
+ * timing breakdown, evaluation rounds and (when configured) the
195
+ * per-canvas visual scores — before it is projected onto the
196
+ * `UiGenerateResult` envelope, which carries only what the protocol
197
+ * needs. Lets a host record or forward the complete run without
198
+ * re-deriving it from the envelope. Bound ONCE at construction
199
+ * (mirrors `onRetry`); not invoked when generation fails. An
200
+ * exception thrown by the observer is the caller's own and fails
201
+ * that generation like any other error on the path — keep it
202
+ * non-throwing.
203
+ */
204
+ readonly onGenerated?: (result: GenerationResult) => void;
205
+ /**
206
+ * Which generation triad to run. `constrained` (default) is the
207
+ * design-system triad — byte-identical to the generator's behaviour
208
+ * before this option existed. `free` relaxes only the design
209
+ * vocabulary (raw HTML, inline CSS, literal spacing / typography /
210
+ * radius; the design package importable but optional) while keeping
211
+ * every hard contract: the boilerplate-typed wire hooks, the import
212
+ * allowlist + security bans, the host envelope, the budgets, the
213
+ * blueprint-reuse shape, and brand-bearing color on the closed
214
+ * `--ggui-color-*` token manifest. Reaches the prompt, boilerplate,
215
+ * tier-0 gate, axis checks and evaluator criteria through the harness.
216
+ * The rendering canvas the `free` prompt states is derived from
217
+ * `input.rendering` (device × shell, refined by viewport width).
218
+ */
219
+ readonly designMode?: DesignMode;
166
220
  }
167
221
 
168
222
  export function createUiGenerator(
@@ -172,10 +226,14 @@ export function createUiGenerator(
172
226
  enableRuntimeRender = false,
173
227
  maxAttempts,
174
228
  maxEvalRounds,
229
+ evaluation,
230
+ visualEvaluation,
175
231
  qualityConfig,
176
232
  gadgetCatalog,
177
233
  disableEnvMutation = false,
178
234
  onRetry,
235
+ onGenerated,
236
+ designMode,
179
237
  } = options;
180
238
 
181
239
  const identity = resolveIdentity(options);
@@ -268,9 +326,14 @@ export function createUiGenerator(
268
326
  const promptWithRendering = rendering
269
327
  ? injectRenderingContext(input.request.prompt, rendering)
270
328
  : input.request.prompt;
329
+ // Canvas class for the `free` prompt — TOLD, never guessed.
330
+ // Ignored by the constrained prompt (its shell/screen
331
+ // descriptors stay byte-identical).
332
+ const canvas = rendering ? canvasForRenderingContext(rendering) : undefined;
271
333
  const promptWithVariance = injectVariance(
272
334
  promptWithRendering,
273
335
  input.variance,
336
+ { profileDeclared: hasProfile(input.profile) },
274
337
  );
275
338
 
276
339
  // Resolve appGadgets by precedence:
@@ -293,6 +356,19 @@ export function createUiGenerator(
293
356
  resolvedAppGadgets,
294
357
  );
295
358
 
359
+ // `effort` (ggui#1059): a named level's dials override the
360
+ // deployment's options for THIS generation; absent ⇒ untouched.
361
+ // Prompt bytes never move with it (dials only).
362
+ const dials = effortDials(input.profile?.effort);
363
+ const effectiveMaxAttempts = dials?.maxAttempts ?? maxAttempts;
364
+ const effectiveMaxEvalRounds = dials?.maxEvalRounds ?? maxEvalRounds;
365
+ const effectiveEvaluation =
366
+ dials !== undefined && evaluation !== undefined ? { ...evaluation, maxRounds: dials.maxEvalRounds } : evaluation;
367
+ const effectiveVisualEvaluation =
368
+ dials !== undefined && visualEvaluation !== undefined
369
+ ? { ...visualEvaluation, passThreshold: dials.selfEvalPassThreshold }
370
+ : visualEvaluation;
371
+
296
372
  const result = await dispatchGeneration({
297
373
  provider,
298
374
  userPrompt,
@@ -300,8 +376,10 @@ export function createUiGenerator(
300
376
  tools,
301
377
  ...(input.contract ? { contract: input.contract } : {}),
302
378
  originalPrompt: input.request.prompt,
303
- ...(maxAttempts !== undefined ? { maxAttempts } : {}),
304
- ...(maxEvalRounds !== undefined ? { maxEvalRounds } : {}),
379
+ ...(effectiveMaxAttempts !== undefined ? { maxAttempts: effectiveMaxAttempts } : {}),
380
+ ...(effectiveMaxEvalRounds !== undefined ? { maxEvalRounds: effectiveMaxEvalRounds } : {}),
381
+ ...(effectiveEvaluation !== undefined ? { evaluation: effectiveEvaluation } : {}),
382
+ ...(effectiveVisualEvaluation !== undefined ? { visualEvaluation: effectiveVisualEvaluation } : {}),
305
383
  ...(qualityConfig !== undefined ? { qualityConfig } : {}),
306
384
  ...(resolvedAppGadgets !== undefined
307
385
  ? { appGadgets: resolvedAppGadgets }
@@ -316,12 +394,19 @@ export function createUiGenerator(
316
394
  enableRuntimeRender,
317
395
  ...(routeOverride !== undefined ? { routeOverride } : {}),
318
396
  ...(onRetry !== undefined ? { onRetry } : {}),
397
+ ...(designMode !== undefined ? { designMode } : {}),
398
+ ...(canvas !== undefined ? { canvas } : {}),
399
+ ...(input.profile !== undefined ? { profile: input.profile } : {}),
400
+ // Fixture props reach the in-loop probe + visual round only (never the prompt, never identity).
401
+ ...(input.fixtureProps !== undefined ? { fixtureProps: input.fixtureProps } : {}),
319
402
  });
320
403
 
404
+ onGenerated?.(result);
405
+
321
406
  const metadata: GenerationMetadata = {
322
407
  provider: input.llm.provider,
323
408
  generator: identity.slug,
324
- model: input.llm.model,
409
+ model: modelRefOfRoute(input.llm),
325
410
  inputTokens: result.tokens.input,
326
411
  outputTokens: result.tokens.output,
327
412
  latencyMs: Date.now() - startedAt,
@@ -362,7 +447,7 @@ export function createUiGenerator(
362
447
  metadata: {
363
448
  provider: input.llm.provider,
364
449
  generator: identity.slug,
365
- model: input.llm.model,
450
+ model: modelRefOfRoute(input.llm),
366
451
  inputTokens: 0,
367
452
  outputTokens: 0,
368
453
  latencyMs: Date.now() - startedAt,
@@ -381,29 +466,29 @@ export function createUiGenerator(
381
466
  }
382
467
 
383
468
  function resolveIdentity(opts: CreateUiGeneratorOptions): {
384
- slug: string;
469
+ slug: GeneratorId;
385
470
  tier: GeneratorTier;
386
- model: string;
471
+ model: ModelRef;
387
472
  } {
388
473
  const hasSlug = typeof opts.slug === 'string' && opts.slug.length > 0;
389
- const hasTierOrModel = opts.tier !== undefined || opts.model !== undefined;
390
- if (hasSlug && hasTierOrModel) {
474
+ if (hasSlug && opts.tier !== undefined) {
391
475
  throw new Error(
392
- 'createUiGenerator: pass either { slug } or { tier, model } — not both.',
476
+ 'createUiGenerator: pass either { slug } or { tier } — not both.',
393
477
  );
394
478
  }
479
+ const model = opts.model ?? DEFAULT_MODEL;
395
480
  if (hasSlug) {
396
- const parsed = parseGeneratorSlug(opts.slug!);
397
- if (!parsed) {
481
+ const slug = opts.slug!;
482
+ const parsed = parseGeneratorSlug(slug);
483
+ if (!parsed || !isGeneratorId(slug)) {
398
484
  throw new Error(
399
- `createUiGenerator: slug ${JSON.stringify(opts.slug)} is not a valid ui-gen-<tier>-<model> identifier.`,
485
+ `createUiGenerator: slug ${JSON.stringify(slug)} is not a valid ui-gen-<tier> identifier.`,
400
486
  );
401
487
  }
402
- return { slug: opts.slug!, tier: parsed.tier, model: parsed.model };
488
+ return { slug, tier: parsed.tier, model };
403
489
  }
404
490
  const tier = opts.tier ?? DEFAULT_TIER;
405
- const model = opts.model ?? DEFAULT_MODEL;
406
- const slug = formatGeneratorSlug({ tier, model });
491
+ const slug = formatGeneratorSlug({ tier });
407
492
  if (!isValidGeneratorSlug(slug)) {
408
493
  // Unreachable: formatGeneratorSlug throws on malformed components,
409
494
  // so a slug it returns must parse. Defensive check kept to surface
@@ -442,7 +527,7 @@ function mapRendering(
442
527
 
443
528
  function failWithoutMetadata(
444
529
  input: UiGenerateInput,
445
- generatorSlug: string,
530
+ generatorSlug: GeneratorId,
446
531
  startedAt: number,
447
532
  error: GenerationError,
448
533
  ): UiGenerateResult {
@@ -452,7 +537,7 @@ function failWithoutMetadata(
452
537
  metadata: {
453
538
  provider: input.llm.provider,
454
539
  generator: generatorSlug,
455
- model: input.llm.model,
540
+ model: modelRefOfRoute(input.llm),
456
541
  inputTokens: 0,
457
542
  outputTokens: 0,
458
543
  latencyMs: Date.now() - startedAt,
@@ -0,0 +1,192 @@
1
+ // packages/ui-gen/src/design-mode.ts
2
+ //
3
+ // The ONE definition of the `designMode` option and the rendering-canvas
4
+ // vocabulary. Every triad surface (system prompt, boilerplate, tier-0
5
+ // gate, axis checks, evaluator criteria) AND the visual evaluator import
6
+ // these types from here — nothing re-declares the unions.
7
+ //
8
+ // - `constrained` (default) — today's triad: the component composes
9
+ // from the `@ggui-ai/design` primitives, prop enums and spacing
10
+ // scale, and tier-0 enforces the design vocabulary. Byte-identical
11
+ // to the pre-`designMode` behaviour (pinned by
12
+ // `design-mode.pin.test.ts`).
13
+ // - `free` — the design vocabulary is relaxed: raw HTML elements,
14
+ // inline styles and `<style>` blocks are allowed; the design package
15
+ // is importable but optional. Every HARD contract stays: the data
16
+ // contract via the boilerplate-typed wire hooks, the import
17
+ // allowlist + security bans, the host envelope, the budgets, the
18
+ // blueprint-reuse shape, and brand-bearing color via the closed
19
+ // `--ggui-color-*` token manifest (no literals, no fallbacks).
20
+
21
+ export const DESIGN_MODES = ["constrained", "free"] as const;
22
+
23
+ export type DesignMode = (typeof DESIGN_MODES)[number];
24
+
25
+ export const DEFAULT_DESIGN_MODE: DesignMode = "constrained";
26
+
27
+ // ─── Rendering canvas ───────────────────────────────────────────────────────
28
+ //
29
+ // ONE canvas vocabulary shared by the prompt input (the `free` prompt
30
+ // TELLS the agent its canvas; the `constrained` prompt keeps its
31
+ // shell/screen descriptors byte-for-byte) and the visual evaluator (one
32
+ // screenshot + judge score per class at `CANVAS_VIEWPORTS[class]`).
33
+ //
34
+ // A class is derived from DISPLAY MODE + width, never from width alone:
35
+ // an inline chat card is `xs-chat-card` whatever the host window
36
+ // measures (the bubble, not the window, bounds the component); a
37
+ // fullscreen-class surface follows its width through the three
38
+ // breakpoints. `canvasForViewportWidth` is monotonic in width per
39
+ // display mode (pinned by `design-mode.test.ts`).
40
+
41
+ export const CANVAS_CLASSES = [
42
+ "xs-chat-card",
43
+ "mobile-fullscreen-small",
44
+ "md",
45
+ "lg",
46
+ "xl",
47
+ ] as const;
48
+
49
+ export type CanvasClass = (typeof CANVAS_CLASSES)[number];
50
+
51
+ export interface CanvasViewport {
52
+ readonly width: number;
53
+ readonly height: number;
54
+ }
55
+
56
+ /** The viewport (CSS px) each class is rendered + judged at. */
57
+ export const CANVAS_VIEWPORTS: Readonly<Record<CanvasClass, CanvasViewport>> = {
58
+ "xs-chat-card": { width: 400, height: 640 },
59
+ "mobile-fullscreen-small": { width: 390, height: 844 },
60
+ md: { width: 768, height: 1024 },
61
+ lg: { width: 1024, height: 768 },
62
+ xl: { width: 1440, height: 900 },
63
+ };
64
+
65
+ /**
66
+ * The two display modes the canvas derivation distinguishes. `inline`
67
+ * is the chat card (MCP Apps `inline` display mode / the `chat` shell);
68
+ * `fullscreen` is every surface the component owns edge to edge or as
69
+ * a panel (`fullscreen`, `pip`, the `spatial` and `partial` shells) —
70
+ * there the width decides the class.
71
+ */
72
+ export type CanvasDisplayMode = "inline" | "fullscreen";
73
+
74
+ /**
75
+ * Map a shell name (`chat` / `fullscreen` / `spatial` / `partial`) or an
76
+ * MCP Apps display mode (`inline` / `fullscreen` / `pip`) onto the
77
+ * canvas display mode. Only `chat` and `inline` are the inline card;
78
+ * everything else is width-governed.
79
+ */
80
+ /**
81
+ * The display mode a JUDGE canvas stands for (ggui#1100): the inline chat
82
+ * card is `xs-chat-card`; every other canvas is a surface the component
83
+ * owns edge to edge, which the served runtime composes with `fit: 'fill'`
84
+ * — so the judge must too, or it scores a picture the user never sees.
85
+ */
86
+ export function displayModeForCanvas(canvas: CanvasClass): CanvasDisplayMode {
87
+ return canvas === "xs-chat-card" ? "inline" : "fullscreen";
88
+ }
89
+
90
+ export function canvasDisplayModeForShell(shell: string | undefined): CanvasDisplayMode {
91
+ return shell === "chat" || shell === "inline" ? "inline" : "fullscreen";
92
+ }
93
+
94
+ /** Fullscreen-class breakpoints (inclusive lower bounds), CSS px. */
95
+ export const CANVAS_BREAKPOINTS = { md: 600, lg: 1024, xl: 1440 } as const;
96
+
97
+ /**
98
+ * Derive the canvas class from display mode + container width.
99
+ *
100
+ * inline → `xs-chat-card` at every width
101
+ * fullscreen → width < 600 → `mobile-fullscreen-small`
102
+ * 600 – 1023 → `md`
103
+ * 1024 – 1439 → `lg`
104
+ * ≥ 1440 → `xl`
105
+ */
106
+ export function canvasForViewportWidth(mode: CanvasDisplayMode, width: number): CanvasClass {
107
+ if (mode === "inline") return "xs-chat-card";
108
+ if (width < CANVAS_BREAKPOINTS.md) return "mobile-fullscreen-small";
109
+ if (width < CANVAS_BREAKPOINTS.lg) return "md";
110
+ if (width < CANVAS_BREAKPOINTS.xl) return "lg";
111
+ return "xl";
112
+ }
113
+
114
+ /**
115
+ * Map today's shell × screen descriptors onto a canvas class when no
116
+ * width is known. `chat` is always the inline card; `spatial` is a
117
+ * ~600px floating panel (`md`); `fullscreen` follows the screen. Unknown
118
+ * values fall back to `lg` (the widest common fullscreen), matching the
119
+ * skeleton's `fullscreen` / `universal` defaults.
120
+ */
121
+ export function canvasForRendering(
122
+ shellType: string | undefined,
123
+ screen: string | undefined,
124
+ ): CanvasClass {
125
+ const shell = shellType ?? "fullscreen";
126
+ if (canvasDisplayModeForShell(shell) === "inline") return "xs-chat-card";
127
+ if (shell === "spatial") return "md";
128
+ switch (screen ?? "universal") {
129
+ case "mobile":
130
+ return "mobile-fullscreen-small";
131
+ case "tablet":
132
+ return "md";
133
+ default:
134
+ return "lg";
135
+ }
136
+ }
137
+
138
+ export interface CanvasDescriptor {
139
+ /** Short human label. */
140
+ readonly label: string;
141
+ /** Inclusive width range the component must look right across, CSS px. */
142
+ readonly minWidthPx: number;
143
+ readonly maxWidthPx: number;
144
+ /** What the host draws around the component at this canvas. */
145
+ readonly host: string;
146
+ }
147
+
148
+ export const CANVAS_DESCRIPTORS: Readonly<Record<CanvasClass, CanvasDescriptor>> = {
149
+ "xs-chat-card": {
150
+ label: "extra-small chat card",
151
+ minWidthPx: 320,
152
+ maxWidthPx: 480,
153
+ host: "an inline card inside a chat message bubble — the host draws the card border, shadow and rounded corners; the component fills the bubble width and takes its natural height",
154
+ },
155
+ "mobile-fullscreen-small": {
156
+ label: "small mobile fullscreen",
157
+ minWidthPx: 320,
158
+ maxWidthPx: 599,
159
+ host: "a phone-sized viewport the component owns edge to edge — single column, touch targets at least 44px, the component manages its own scroll",
160
+ },
161
+ md: {
162
+ label: "medium panel",
163
+ minWidthPx: 600,
164
+ maxWidthPx: 1023,
165
+ host: "a tablet viewport or a floating panel (spatial / picture-in-picture) — one or two columns, medium density",
166
+ },
167
+ lg: {
168
+ label: "large fullscreen",
169
+ minWidthPx: 1024,
170
+ maxWidthPx: 1439,
171
+ host: "a desktop viewport the component owns edge to edge — multi-column layouts, hover states and pointer interactions are available",
172
+ },
173
+ xl: {
174
+ label: "extra-large fullscreen",
175
+ minWidthPx: 1440,
176
+ maxWidthPx: 1920,
177
+ host: "a wide desktop viewport the component owns edge to edge — cap content width or use the extra room deliberately; never stretch a single column across the whole width",
178
+ },
179
+ };
180
+
181
+ /**
182
+ * The explicit sentence the `free` prompt renders for a canvas — the
183
+ * agent is TOLD the canvas, it never guesses it.
184
+ */
185
+ export function describeCanvas(canvas: CanvasClass): string {
186
+ const d = CANVAS_DESCRIPTORS[canvas];
187
+ return (
188
+ `This component renders on the \`${canvas}\` canvas (${d.label}): ` +
189
+ `${d.minWidthPx}–${d.maxWidthPx}px wide, ${d.host}. ` +
190
+ `Design for the whole range — the host may size the iframe anywhere inside it, and the host owns the height: size to your content, never to the viewport.`
191
+ );
192
+ }
@@ -26,7 +26,7 @@ export const DEFAULT_DESIGN_SYSTEM_DOCS = `# Design System - Default Light Theme
26
26
  - var(--ggui-color-primary-300) - #7dd3fc
27
27
  - var(--ggui-color-primary-400) - #38bdf8
28
28
  - var(--ggui-color-primary-500) - #0ea5e9
29
- - var(--ggui-color-primary-600) - #0284c7 (main action color)
29
+ - var(--ggui-color-primary-600) - #0284c7 (main action FILL — buttons, borders, icon fills; accent TEXT takes the link role below)
30
30
  - var(--ggui-color-primary-700) - #0369a1
31
31
  - var(--ggui-color-primary-800) - #075985
32
32
  - var(--ggui-color-primary-900) - #0c4a6e
@@ -36,20 +36,26 @@ These tokens adapt automatically to any theme (light, dark, branded). **ALWAYS u
36
36
 
37
37
  | Token | CSS Variable | Default | Role |
38
38
  |-------|-------------|---------|------|
39
- | surface | var(--ggui-color-surface) | Main content background |
40
- | onSurface | var(--ggui-color-onSurface) | Primary text on surface |
41
- | surfaceVariant | var(--ggui-color-surfaceVariant) | Card/panel background |
42
- | onSurfaceVariant | var(--ggui-color-onSurfaceVariant) | Muted/secondary text |
43
- | container | var(--ggui-color-container) | Primary-branded containers |
44
- | onContainer | var(--ggui-color-onContainer) | Text on branded containers |
39
+ | ground | var(--ggui-color-ground) | The page / canvas behind everything |
40
+ | onGround | var(--ggui-color-onGround) | Text on the canvas |
41
+ | container | var(--ggui-color-container) | A card, a bubble, a panel — the unit of content |
42
+ | onContainer | var(--ggui-color-onContainer) | Body text on a card |
43
+ | elevated | var(--ggui-color-elevated) | Menus, popovers, modals, toasts — floats above a card |
44
+ | onElevated | var(--ggui-color-onElevated) | Text on an elevated surface |
45
+ | heroGround | var(--ggui-color-heroGround) | A hero / greeting panel's ground — brand-tinted on a light host, the ink pair on a dark one (Card surface="hero") |
46
+ | onHeroGround | var(--ggui-color-onHeroGround) | Text on the hero ground |
47
+ | sunken | var(--ggui-color-sunken) | Wells, inputs at rest, code blocks — recessed inside a card |
48
+ | onSunken | var(--ggui-color-onSunken) | Muted / secondary text |
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 |
45
50
  | outline | var(--ggui-color-outline) | Borders, dividers |
46
51
  | outlineVariant | var(--ggui-color-outlineVariant) | Subtle borders |
47
52
 
48
53
  **Usage pattern:**
49
- - Page/section background → \`var(--ggui-color-surface)\`
50
- - Body text → \`var(--ggui-color-onSurface)\`
51
- - Card/panel background → \`var(--ggui-color-surfaceVariant)\`
52
- - Secondary/muted text → \`var(--ggui-color-onSurfaceVariant)\`
54
+ - Page/section background → \`var(--ggui-color-ground)\`
55
+ - Body text → \`var(--ggui-color-onContainer)\`
56
+ - Card/panel background → \`var(--ggui-color-container)\`; recessed wells / inputs → \`var(--ggui-color-sunken)\`
57
+ - Secondary/muted text → \`var(--ggui-color-onSunken)\`
58
+ - Accent / branded text (eyebrow, tagline, link, label, inline arrow) → \`var(--ggui-color-link)\` or \`<Text tone="emphasized">\` — never a bare \`primary-<stop>\` as a text color
53
59
  - Branded section/header → \`var(--ggui-color-container)\` bg + \`var(--ggui-color-onContainer)\` text
54
60
  - Borders/dividers → \`var(--ggui-color-outline)\` or \`var(--ggui-color-outlineVariant)\`
55
61
 
@@ -66,11 +72,11 @@ Prefer the primitives' \`tone="success|warning|error|info"\` prop over hand-writ
66
72
  - **NEVER** use rgba(), hsl(), or other CSS color functions with hardcoded values.
67
73
  - **NEVER** use raw neutral-* or gray-* scale tokens (neutral-50, neutral-900, etc.) — these are internal to the theme and break in dark mode.
68
74
  - **ALWAYS** use semantic tokens for text and backgrounds:
69
- - Text: \`var(--ggui-color-onSurface)\` or \`var(--ggui-color-onSurfaceVariant)\`
70
- - Backgrounds: \`var(--ggui-color-surface)\` or \`var(--ggui-color-surfaceVariant)\`
75
+ - Text: \`var(--ggui-color-onContainer)\` or \`var(--ggui-color-onSunken)\`
76
+ - Backgrounds: \`var(--ggui-color-container)\` or \`var(--ggui-color-sunken)\`
71
77
  - Borders: \`var(--ggui-color-outline)\` or \`var(--ggui-color-outlineVariant)\`
72
78
  - For branded elements use \`var(--ggui-color-primary-*)\` scale tokens — these ARE safe because primary adapts per theme.
73
- - For card backgrounds use \`var(--ggui-color-surfaceVariant)\` or \`var(--ggui-color-primary-50)\`
79
+ - For card backgrounds use \`var(--ggui-color-container)\` or \`var(--ggui-color-primary-50)\`
74
80
 
75
81
  ## Spacing
76
82
 
@@ -165,16 +171,17 @@ Use \`var(--ggui-spacing-N)\` for all padding, gap, and margin values. **Never u
165
171
  \`\`\`
166
172
 
167
173
  ### Color usage guide
168
- - **Semantic roles** (surface, onSurface, container, outline): Use for all surface/text/border decisions — these adapt to any theme
174
+ - **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
169
175
  - **primary-50/100**: Section backgrounds, highlight strips, card headers
170
176
  - **primary-200/300**: Borders, dividers, focus rings, input outlines
171
- - **primary-500/600**: Icons, links, labels, badges, buttons, CTAs
177
+ - **primary-500/600**: Filled buttons, CTAs, badges, icon fills, active indicators — fills and borders, NOT a text color
178
+ - **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\`)
172
179
  - **primary-700/800/900**: Headings and text on light primary backgrounds
173
180
  - **neutral-***: Only when you need a specific shade that semantic tokens don't cover
174
181
 
175
182
  The primary palette is the app's brand — use it throughout (headers, accents, borders, interactive elements), not just on the submit button.
176
183
 
177
- **Note:** Do NOT add fallback values to var() (e.g., var(--ggui-color-surface, #fafafa)). Just use var(--ggui-color-surface) — the theme provides all values.
184
+ **Note:** Do NOT add fallback values to var() (e.g., var(--ggui-color-container, #fafafa)). Just use var(--ggui-color-container) — the theme provides all values.
178
185
 
179
186
  ## Motion & Animation
180
187
 
package/src/effort.ts ADDED
@@ -0,0 +1,48 @@
1
+ // The `effort` reader table (ggui#1059, A2). The wire carries ONLY the level
2
+ // name (`generation.profile.effort`, protocol #1058); this table is the
3
+ // cold-gen READER that maps a name onto the harness's dials, versioned here
4
+ // so a level's meaning (and its price line) can move without a wire change.
5
+ //
6
+ // Boundaries, stated once:
7
+ // - `effort` renders NO prompt text — the prompt is byte-identical under any
8
+ // level (`profile-cross.pin.test.ts`). Dials only.
9
+ // - Absent ⇒ the deployment's own options, untouched. The table is applied
10
+ // only when a level is named.
11
+ // - `selfEvalPassThreshold` is the GENERATOR's in-loop visual
12
+ // self-evaluation threshold (`visualEvaluation.passThreshold`). A serving
13
+ // layer's own acceptance bar is a different number and does not move with
14
+ // `effort`.
15
+ // - `modelTier` / `judgeTier` are REQUESTS to the deployment's router
16
+ // (`fast | balanced | premium`), never a model id: a deployment with one
17
+ // key is never promoted by a profile. The harness does not act on them;
18
+ // the caller that owns model selection reads them from this table.
19
+ import type { AppGenerationProfileEffort, ModelTier } from "@ggui-ai/protocol";
20
+
21
+ /** Bump when a level's meaning changes; the price line beside it follows. */
22
+ export const EFFORT_TABLE_VERSION = 1;
23
+
24
+ export interface EffortDials {
25
+ /** Coding-loop turn cap (`maxAttempts`). */
26
+ readonly maxAttempts: number;
27
+ /** Evaluation rounds the harness may spend (`maxEvalRounds`). */
28
+ readonly maxEvalRounds: number;
29
+ /** The generator's in-loop visual self-evaluation threshold, 0–100 — not a serving layer's acceptance bar. */
30
+ readonly selfEvalPassThreshold: number;
31
+ /** Requested coding-model tier — a request to the deployment's router, never a model id. */
32
+ readonly modelTier: ModelTier;
33
+ /** Requested judge-model tier — same nature. */
34
+ readonly judgeTier: ModelTier;
35
+ }
36
+
37
+ export const EFFORT_DIALS: Readonly<Record<AppGenerationProfileEffort, EffortDials>> = {
38
+ low: { maxAttempts: 5, maxEvalRounds: 1, selfEvalPassThreshold: 65, modelTier: "fast", judgeTier: "fast" },
39
+ medium: { maxAttempts: 8, maxEvalRounds: 2, selfEvalPassThreshold: 70, modelTier: "fast", judgeTier: "balanced" },
40
+ high: { maxAttempts: 8, maxEvalRounds: 3, selfEvalPassThreshold: 75, modelTier: "balanced", judgeTier: "balanced" },
41
+ xhigh: { maxAttempts: 10, maxEvalRounds: 3, selfEvalPassThreshold: 80, modelTier: "premium", judgeTier: "balanced" },
42
+ ultra: { maxAttempts: 12, maxEvalRounds: 4, selfEvalPassThreshold: 85, modelTier: "premium", judgeTier: "premium" },
43
+ };
44
+
45
+ /** The dials for a named level; `undefined` when no level is named (the deployment's options stand). */
46
+ export function effortDials(effort: AppGenerationProfileEffort | undefined): EffortDials | undefined {
47
+ return effort === undefined ? undefined : EFFORT_DIALS[effort];
48
+ }