fix(ai): JSONL sub-agent path emits design-system refs not hex
Two-part fix for the web-app chat path (both built-in and CLI mode go through
sub-agent JSONL output, not MCP tool calls — `jsonl-format.md` line 50 forbids
tool calls). Without this, every fill in generated designs was a hex literal
even after the 5/3 design-system-aware work — the 188 v1 element tools and
DEFAULT_PALETTE_FALLBACK were dead weight here.
A. STYLE GUIDE injection now uses ref + (hex) double form so the model sees
`$color-accent` paired with the resolved hex it represents:
Before: - Background: #FFF8F0 Surface: #FFFFFF
After: - Background: `$color-bg-deep` (resolves to #FFF8F0)
- Surface: `$color-surface` (#FFFFFF)
Applied to both `buildSubAgentStyleGuideInstruction` (selectedStyleGuideContent
path) and the inline `plan.styleGuide` injection in orchestrator-sub-agent.ts.
B. `seedDocVariablesFromStyleGuide` runs once before sub-agent execution: when
`doc.variables` is empty AND a style guide is selected, it maps the palette
to v1 token names (`color-bg-deep` / `color-accent` / etc) and seeds them
into `doc.variables`. This makes refs emitted by the model resolve to the
user's chosen palette at render time instead of falling back to the default
#2563EB blue.
A and B are coupled — A alone would make designs render in the wrong color
(every design becomes blue regardless of style guide); B alone leaves the model
mimicking the hex from the prompt. Both must ship together.
Also:
- jsonl-format.md: DESIGN SYSTEM TOKENS section + example fills converted to
refs + CRITICAL contract moved to top (so future budget overruns can't
truncate it). Budget bumped 1500 → 1700 for safety margin.
- elements.md: Theme handling section now flags MCP path vs JSONL path so
models on either path know which guidance applies.
This commit is contained in:
parent
e80326473d
commit
e0039a303e
|
|
@ -74,12 +74,13 @@ platform: mobile
|
|||
| Button | 12px |
|
||||
`;
|
||||
|
||||
it('summarizes style guide tokens for non-full tiers', () => {
|
||||
it('summarizes style guide tokens for non-full tiers using ref + (hex) form', () => {
|
||||
const summary = buildSubAgentStyleGuideInstruction(styleGuide, 'dark-bold-mobile', 'basic');
|
||||
|
||||
expect(summary).toContain('VISUAL STYLE GUIDE SUMMARY');
|
||||
expect(summary).toContain('Background: #111111');
|
||||
expect(summary).toContain('Accent: #22C55E');
|
||||
expect(summary).toContain('`$color-bg-deep` (resolves to #111111)');
|
||||
expect(summary).toContain('`$color-accent` (resolves to #22C55E)');
|
||||
expect(summary).toContain('EMIT REFS');
|
||||
expect(summary).not.toContain('## Color System');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -80,10 +80,6 @@ export function buildSubAgentStyleGuideInstruction(
|
|||
styleGuideName: string | undefined,
|
||||
tier: 'full' | 'standard' | 'basic',
|
||||
): string {
|
||||
if (tier === 'full') {
|
||||
return `VISUAL STYLE GUIDE (follow these specifications exactly):\n${content}`;
|
||||
}
|
||||
|
||||
const values = extractStyleGuideValues(content);
|
||||
const tags = styleGuideName
|
||||
? styleGuideRegistry
|
||||
|
|
@ -92,19 +88,36 @@ export function buildSubAgentStyleGuideInstruction(
|
|||
.join(', ')
|
||||
: undefined;
|
||||
|
||||
// The palette below is SEEDED into doc.variables when the sub-agent runs (see
|
||||
// seedDocVariablesFromStyleGuide in orchestrator.ts), so emitting `$color-*`
|
||||
// refs in JSONL fills is both correct AND visually accurate — refs resolve to
|
||||
// the hex shown in parens.
|
||||
const refLine = (label: string, ref: string, hex: string | undefined) =>
|
||||
hex ? `- ${label}: \`${ref}\` (resolves to ${hex})` : null;
|
||||
|
||||
const refLines = [
|
||||
refLine('Background', '$color-bg-deep', values.colors.background),
|
||||
refLine('Surface', '$color-surface', values.colors.surface),
|
||||
refLine('Accent', '$color-accent', values.colors.accent),
|
||||
refLine('Text', '$color-text-primary', values.colors.textPrimary),
|
||||
refLine('Secondary text', '$color-text-body', values.colors.textSecondary),
|
||||
refLine('Muted text', '$color-text-muted', values.colors.textMuted),
|
||||
refLine('Border', '$color-border', values.colors.border),
|
||||
].filter(Boolean);
|
||||
|
||||
if (tier === 'full') {
|
||||
return `VISUAL STYLE GUIDE (follow these specifications exactly):\n${content}\n\nDESIGN-SYSTEM REFS — these are SEEDED into doc.variables. Emit \`$color-*\` refs in JSONL fills (NOT hex) so the design respects the user's tokens at render time:\n${refLines.join('\n')}`;
|
||||
}
|
||||
|
||||
const lines = [
|
||||
`VISUAL STYLE GUIDE SUMMARY${styleGuideName ? ` (${styleGuideName})` : ''}:`,
|
||||
tags ? `- Tags: ${tags}` : null,
|
||||
values.colors.background ? `- Background: ${values.colors.background}` : null,
|
||||
values.colors.surface ? `- Surface: ${values.colors.surface}` : null,
|
||||
values.colors.accent ? `- Accent: ${values.colors.accent}` : null,
|
||||
values.colors.textPrimary ? `- Text: ${values.colors.textPrimary}` : null,
|
||||
values.colors.textSecondary ? `- Secondary text: ${values.colors.textSecondary}` : null,
|
||||
...refLines,
|
||||
values.typography.displayFont ? `- Heading font: ${values.typography.displayFont}` : null,
|
||||
values.typography.bodyFont ? `- Body font: ${values.typography.bodyFont}` : null,
|
||||
values.radius.card != null ? `- Card radius: ${values.radius.card}` : null,
|
||||
values.radius.button != null ? `- Button radius: ${values.radius.button}` : null,
|
||||
'- Match the selected guide using these tokens; do not invent a conflicting palette.',
|
||||
'- These `$color-*` refs are SEEDED into doc.variables. EMIT REFS in your JSONL fills (NOT hex) — they resolve to the hex above at render time. Do not invent a conflicting palette.',
|
||||
];
|
||||
|
||||
return lines.filter(Boolean).join('\n');
|
||||
|
|
|
|||
|
|
@ -774,10 +774,16 @@ CRITICAL LAYOUT CONSTRAINTS:
|
|||
} else if (plan.styleGuide) {
|
||||
const sg = plan.styleGuide;
|
||||
const p = sg.palette;
|
||||
prompt += `\n\nSTYLE GUIDE (use these consistently):
|
||||
- Background: ${p.background} Surface: ${p.surface}
|
||||
- Text: ${p.text} Secondary: ${p.secondary}
|
||||
- Accent: ${p.accent} Accent2: ${p.accent2} Border: ${p.border}
|
||||
// The palette below is SEEDED into doc.variables (see
|
||||
// seedDocVariablesFromStyleGuide in orchestrator.ts). Emit `$color-*` refs
|
||||
// in JSONL fills — they resolve to the hex shown in parens at render time.
|
||||
prompt += `\n\nSTYLE GUIDE — these refs are SEEDED into doc.variables; EMIT REFS in your JSONL fills (NOT hex):
|
||||
- Background: \`$color-bg-deep\` (resolves to ${p.background})
|
||||
- Surface: \`$color-surface\` (${p.surface})
|
||||
- Text: \`$color-text-primary\` (${p.text})
|
||||
- Secondary: \`$color-text-body\` (${p.secondary})
|
||||
- Accent: \`$color-accent\` (${p.accent})
|
||||
- Border: \`$color-border\` (${p.border})
|
||||
- Heading font: ${sg.fonts.heading} Body font: ${sg.fonts.body}
|
||||
- Aesthetic: ${sg.aesthetic}`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import type { DesignMdSpec } from '@/types/design-md';
|
|||
import { streamChat } from './ai-service';
|
||||
import { resolveSkills } from '@zseven-w/pen-ai-skills';
|
||||
import { styleGuideRegistry } from '@zseven-w/pen-ai-skills/_generated/style-guide-registry';
|
||||
import { selectStyleGuide } from '@zseven-w/pen-ai-skills/style-guide';
|
||||
import { extractStyleGuideValues, selectStyleGuide } from '@zseven-w/pen-ai-skills/style-guide';
|
||||
import {
|
||||
getOrchestratorTimeouts,
|
||||
prepareDesignPrompt,
|
||||
|
|
@ -482,6 +482,54 @@ function reorderDashboardMainChildren(plan: OrchestratorPlan, mainParentId: stri
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Seed `doc.variables` with the plan's style-guide palette (mapped to v1
|
||||
* semantic token names) so sub-agent JSONL output containing `$color-*` refs
|
||||
* resolves to the user's chosen palette at render time, not the
|
||||
* DEFAULT_PALETTE_FALLBACK blue.
|
||||
*
|
||||
* Without this, the sub-agent prompt teaches the model to emit
|
||||
* `$color-accent` (see `buildSubAgentStyleGuideInstruction` in
|
||||
* `orchestrator-sub-agent-compact.ts`) but the renderer would fall back to
|
||||
* the default `#2563EB` blue — completely overriding any warm/orange/etc
|
||||
* style guide picked by the planner.
|
||||
*
|
||||
* Skipped when `doc.variables` is already non-empty: respects user-seeded
|
||||
* tokens (design.md, custom palette) without overwriting.
|
||||
*/
|
||||
export function seedDocVariablesFromStyleGuide(plan: OrchestratorPlan): void {
|
||||
const store = useDocumentStore.getState();
|
||||
const existing = store.document.variables ?? {};
|
||||
if (Object.keys(existing).length > 0) return;
|
||||
|
||||
const palette: Record<string, string> = {};
|
||||
|
||||
if (plan.styleGuide?.palette) {
|
||||
const p = plan.styleGuide.palette;
|
||||
palette['color-bg-deep'] = p.background;
|
||||
palette['color-surface'] = p.surface;
|
||||
palette['color-text-primary'] = p.text;
|
||||
palette['color-text-body'] = p.secondary;
|
||||
palette['color-accent'] = p.accent;
|
||||
palette['color-border'] = p.border;
|
||||
} else if (plan.selectedStyleGuideContent) {
|
||||
const v = extractStyleGuideValues(plan.selectedStyleGuideContent).colors;
|
||||
if (v.background) palette['color-bg-deep'] = v.background;
|
||||
if (v.surface) palette['color-surface'] = v.surface;
|
||||
if (v.textPrimary) palette['color-text-primary'] = v.textPrimary;
|
||||
if (v.textSecondary) palette['color-text-body'] = v.textSecondary;
|
||||
if (v.textMuted) palette['color-text-muted'] = v.textMuted;
|
||||
if (v.accent) palette['color-accent'] = v.accent;
|
||||
if (v.border) palette['color-border'] = v.border;
|
||||
}
|
||||
|
||||
if (Object.keys(palette).length === 0) return;
|
||||
|
||||
for (const [name, value] of Object.entries(palette)) {
|
||||
store.setVariable(name, { type: 'color', value });
|
||||
}
|
||||
}
|
||||
|
||||
import { applyAppendContextToPlan } from './orchestrator-append';
|
||||
export { applyAppendContextToPlan } from './orchestrator-append';
|
||||
export type { AppendPlanResult } from './orchestrator-append';
|
||||
|
|
@ -866,6 +914,8 @@ export async function executeOrchestration(
|
|||
totalNodes: 0,
|
||||
};
|
||||
|
||||
seedDocVariablesFromStyleGuide(plan);
|
||||
|
||||
let results: SubAgentResult[];
|
||||
try {
|
||||
results = await executeSubAgents(
|
||||
|
|
|
|||
|
|
@ -50,6 +50,8 @@ Multi-tool example — a "Notifications" settings section with a header + 4 togg
|
|||
|
||||
## Theme handling — when to pass `theme: 'system'`
|
||||
|
||||
> **This section applies to the MCP tool-call path only** (codex CLI / Claude Code / Cursor calling `add_X_v1` directly). The web-app sub-agent JSONL path forbids tool calls — there, write `$color-*` / `$type-*` / `$spacing-*` / `$radius-*` refs directly in JSONL fills/fontSize/etc. See the **DESIGN SYSTEM TOKENS** section in `jsonl-format.md` for the available token names and when refs vs literals are appropriate.
|
||||
|
||||
**Default to `theme: 'system'`** for every v1 tool call. This makes the output respect the user's design system (`doc.variables` / `doc.themes`):
|
||||
|
||||
- If user has seeded `applySemanticPalette(doc)` or set custom token values, v1 'system' mode emits `$color-*` / `$type-*` / `$spacing-*` / `$radius-*` refs that resolve to user's design system at paint time.
|
||||
|
|
|
|||
|
|
@ -4,18 +4,20 @@ description: Sub-agent flat JSONL output format with node types and rules
|
|||
phase: [generation]
|
||||
trigger: null
|
||||
priority: 0
|
||||
budget: 1500
|
||||
budget: 1700
|
||||
category: base
|
||||
---
|
||||
|
||||
CRITICAL: Output ONLY the `json block. Do NOT write any text, explanation, plan, tool calls, or function calls. Do NOT use [TOOL_CALL] or {tool => ...} syntax. Start your response with `json immediately.
|
||||
|
||||
PenNode flat JSONL engine. Output a ```json block with ONE node per line.
|
||||
|
||||
TYPES:
|
||||
frame (width,height,layout,gap,padding,justifyContent,alignItems,clipContent,cornerRadius,fill,stroke,effects), rectangle, ellipse, text (content,fontFamily,fontSize,fontWeight,fontStyle,fill,width,textAlign,textGrowth,lineHeight,letterSpacing), icon_font (iconFontName,width,height,fill), path (d,width,height,fill,stroke), image (width,height,imageSearchQuery,imagePrompt). imagePrompt: describe subject+scene+style, NEVER mention background type (transparent/white/plain). Match composition to aspect ratio.
|
||||
SHARED: id, type, name, role, x, y, opacity
|
||||
ROLES: section, row, column, divider | navbar, button, icon-button, badge, input, search-bar | card, stat-card, pricing-card, feature-card | heading, subheading, body-text, caption, label | table, table-row, table-header
|
||||
width/height: number | "fill_container" | "fit_content". padding: number | [v,h] | [T,R,B,L]. Fill=[{"type":"solid","color":"#hex"}].
|
||||
Stroke: {"thickness":N,"fill":[{"type":"solid","color":"#hex"}]}. Directional: {"thickness":{"bottom":1},"fill":[...]}.
|
||||
width/height: number | "fill_container" | "fit_content". padding: number | [v,h] | [T,R,B,L]. Fill=[{"type":"solid","color":"#hex" | "$color-*"}].
|
||||
Stroke: {"thickness":N,"fill":[{"type":"solid","color":"#hex" | "$color-*"}]}. Directional: {"thickness":{"bottom":1},"fill":[...]}.
|
||||
|
||||
RULES:
|
||||
|
||||
|
|
@ -33,18 +35,24 @@ RULES:
|
|||
- FORMS: ALL inputs AND button use width="fill_container". gap=16-20.
|
||||
- Z-order: Earlier siblings render on top. Overlay elements (badges, indicators, floating buttons) MUST come BEFORE the content they overlap.
|
||||
|
||||
DESIGN SYSTEM TOKENS — prefer refs over literals so output respects the user's design system. The renderer resolves refs against `doc.variables` (or a default light palette when un-seeded), so refs are SAFE even when the doc has no design system seeded yet.
|
||||
|
||||
- COLORS: `$color-{bg-deep|surface|surface-2|surface-3|border|border-strong|text-primary|text-body|text-muted|text-subtle|accent|destructive|success|scrim|info-bg|info-text|success-bg|success-text|warning-bg|warning-text|danger-bg|danger-text|chart-1..6}`. Light defaults: bg-deep `#F8FAFC`, surface `#FFFFFF`, surface-2 `#F1F5F9`, border `#E2E8F0`, text-primary `#0F172A`, text-body `#334155`, text-muted `#64748B`, text-subtle `#94A3B8`, accent `#2563EB`, destructive `#EF4444`, success `#10B981`.
|
||||
- TYPOGRAPHY: `$type-{display|h1|h2|h3|body|caption}-{size|weight|line-height}`. Defaults: display 64/700/1.0, h1 24/600/1.2, h2 20/600/1.25, h3 16/600/1.3, body 14/400/1.5, caption 12/400/1.4. Plus `$type-display-letter-spacing` (-0.5), `$type-uppercase-label-letter-spacing` (1.5).
|
||||
- SPACING / RADIUS: `$spacing-{1|2|3|4|5}` = 4/8/12/16/24 px. `$radius-{sm|md|lg}` = 4/8/12 px.
|
||||
|
||||
USE refs for: standard semantic colors (page bg, surfaces, text levels, borders, accent, alerts, charts), and typography sizes/weights/line-heights that match the scale above. KEEP literal hex / numbers for: brand-specific colors not in the palette (custom logo color, off-palette accent), pixel values that don't match the typography scale, and "white text on accent" (`#FFFFFF`).
|
||||
|
||||
FORMAT: \_parent (null=root, else parent-id). Parent before children.
|
||||
|
||||
```json
|
||||
{"_parent":null,"id":"root","type":"frame","name":"Hero","width":"fill_container","height":"fit_content","layout":"vertical","gap":24,"padding":[48,24],"fill":[{"type":"solid","color":"#F8FAFC"}]}
|
||||
{"_parent":null,"id":"root","type":"frame","name":"Hero","width":"fill_container","height":"fit_content","layout":"vertical","gap":24,"padding":[48,24],"fill":[{"type":"solid","color":"$color-bg-deep"}]}
|
||||
{"_parent":"root","id":"header","type":"frame","name":"Header","justifyContent":"space_between","alignItems":"center","width":"fill_container"}
|
||||
{"_parent":"header","id":"logo","type":"text","name":"Logo","content":"ACME","fontSize":18,"fontWeight":600,"fontFamily":"Space Grotesk","fill":[{"type":"solid","color":"#0D0D0D"}]}
|
||||
{"_parent":"header","id":"logo","type":"text","name":"Logo","content":"ACME","fontSize":18,"fontWeight":600,"fontFamily":"Space Grotesk","fill":[{"type":"solid","color":"$color-text-primary"}]}
|
||||
{"_parent":"header","id":"notifBtn","type":"frame","name":"Notification","width":44,"height":44}
|
||||
{"_parent":"notifBtn","id":"notifIcon","type":"icon_font","name":"Bell","iconFontName":"bell","width":20,"height":20,"fill":"#0D0D0D","x":12,"y":12}
|
||||
{"_parent":"root","id":"title","type":"text","name":"Headline","content":"Learn Smarter","fontSize":48,"fontWeight":700,"fontFamily":"Space Grotesk","lineHeight":0.95,"fill":[{"type":"solid","color":"#0F172A"}]}
|
||||
{"_parent":"root","id":"desc","type":"text","name":"Description","content":"AI-powered vocabulary learning that adapts to your pace","fontSize":16,"textGrowth":"fixed-width","width":"fill_container","lineHeight":1.5,"fill":[{"type":"solid","color":"#64748B"}]}
|
||||
{"_parent":"root","id":"cta","type":"frame","name":"CTA Button","padding":[14,28],"cornerRadius":10,"justifyContent":"center","fill":[{"type":"solid","color":"#2563EB"}]}
|
||||
{"_parent":"notifBtn","id":"notifIcon","type":"icon_font","name":"Bell","iconFontName":"bell","width":20,"height":20,"fill":"$color-text-primary","x":12,"y":12}
|
||||
{"_parent":"root","id":"title","type":"text","name":"Headline","content":"Learn Smarter","fontSize":48,"fontWeight":700,"fontFamily":"Space Grotesk","lineHeight":0.95,"fill":[{"type":"solid","color":"$color-text-primary"}]}
|
||||
{"_parent":"root","id":"desc","type":"text","name":"Description","content":"AI-powered vocabulary learning that adapts to your pace","fontSize":16,"textGrowth":"fixed-width","width":"fill_container","lineHeight":1.5,"fill":[{"type":"solid","color":"$color-text-muted"}]}
|
||||
{"_parent":"root","id":"cta","type":"frame","name":"CTA Button","padding":[14,28],"cornerRadius":10,"justifyContent":"center","fill":[{"type":"solid","color":"$color-accent"}]}
|
||||
{"_parent":"cta","id":"cta-text","type":"text","name":"CTA Label","content":"Get Started","fontSize":16,"fontWeight":600,"fill":[{"type":"solid","color":"#FFFFFF"}]}
|
||||
```
|
||||
|
||||
CRITICAL: Output ONLY the `json block. Do NOT write any text, explanation, plan, tool calls, or function calls. Do NOT use [TOOL_CALL] or {tool => ...} syntax. Start your response with `json immediately.
|
||||
|
|
|
|||
Loading…
Reference in a new issue