feat(chat): 下线意图澄清里的设计风格选择卡片 (OPEND-2760)

提示词侧不再让 agent 出「设计风格」这一类 question-form:
direction-picker / discovery-question-form 两个 atom 的 SKILL 与
daemon/contracts 的 prompts 同步删掉该问法。前端 visual-style-catalog
与 visual-style-deck 保留但不再被澄清轮引用,组件代码加注释说明可能找回。

用户裁决:「这些代码先讲提示词干掉, 组件代码注释, 后续可能要找回」。
od-next 策略分支核对过,其澄清轮本来就不枚举问题类型,无需改动。
This commit is contained in:
lefarcen
2026-09-07 21:11:15 +08:00
parent 6863b5f7b3
commit 501eb5640a
25 changed files with 726 additions and 157 deletions
+2 -4
View File
@@ -174,7 +174,7 @@ Choose only from questions that remain unanswered and genuinely affect the desig
#### 4. Control Types
Supported \`type\` values are: \`radio\`, \`checkbox\`, \`select\`, \`text\`, \`textarea\`, \`number\`, \`range\`, \`date\`, \`time\`, \`datetime-local\`, \`color\`, \`url\`, \`email\`, \`tel\`, \`file\`, \`switch\`, and \`direction-cards\`.
Supported \`type\` values are: \`radio\`, \`checkbox\`, \`select\`, \`text\`, \`textarea\`, \`number\`, \`range\`, \`date\`, \`time\`, \`datetime-local\`, \`color\`, \`url\`, \`email\`, \`tel\`, \`file\`, and \`switch\`.
Special rules:
- At most 6-7 options per question; merge near-duplicates instead of listing more.
@@ -185,13 +185,12 @@ Special rules:
- Use \`maxSelections\` when a \`checkbox\` question needs a selection limit.
- A \`file\` question may allow multiple files with \`multiple: true\`, but the answer returns filenames only, not file contents.
- Use \`direction-cards\` only when the user explicitly asks to see visual directions. It is a trigger for Open Design's host-owned visual-style catalog: emit only the question's stable \`id\`, localized \`label\`, \`type: "direction-cards"\`, and \`required\` when appropriate. Omit \`options\`, \`cards\`, \`variant\`, and \`defaultValue\`; the host owns the versioned catalog, previews, recommendation, and stable style ids for the project kind.
- For finite option sets, allow custom input by default: omit \`allowCustom\` or set it to \`true\`. Set it to \`false\` only when downstream systems require fixed machine IDs.
- If the \`brand\` question is included, its \`id\` must be \`brand\`, and its option values must be \`pick_direction\`, \`brand_spec\`, and \`reference_match\`.
#### 5. Recommended Answers
- Based on the brief and known context, provide a sensible default for each non-visual question that is suitable for preselection. A host-owned \`direction-cards\` question is the exception and must not invent a default.
- Based on the brief and known context, provide a sensible default for each non-visual question that is suitable for preselection.
- Use \`defaultValue\` to preselect an answer: provide one option \`value\` for a single-choice question and an array of \`value\` entries for a multiple-choice question.
- You may append "(Recommended)" to the option \`label\` and briefly explain the recommendation in \`description\`.
- \`defaultValue\` must match an option's \`value\`, not its localized label.
@@ -223,7 +222,6 @@ If the user selects \`brand_spec\` or \`reference_match\` without providing an a
- **An active design system is available:** Bind its tokens directly and follow the design system strictly.
- **No design system or brand source is available:** Choose the best-matching option from the runtime's direction library based on the brief's domain, audience, and overall tone, then bind its visual tokens. Do not ask the user again. If a Host-owned direction-form answer supplies \`value\`, \`foundation\`, and \`guidance\`, resolve the library \`foundation\` (not the Host catalogue \`value\`) and apply \`guidance\` as the selected refinement. If the runtime provides only an index of direction IDs and names, first run \`"$OD_NODE_BIN" "$OD_BIN" tools directions --id <id>\` to retrieve the full specification. Never infer colors or fonts from the name alone. If the runtime provides the complete direction library inline, use the inline specification directly.
- Send \`direction-cards\` only when the user explicitly asks to see direction options. Never send them proactively.
### 2. Plan
+11
View File
@@ -184,6 +184,17 @@ export const DESIGN_DIRECTIONS: DesignDirection[] = [
];
/**
* ⚠️ **休眠件(T69,2026-09-07)** —— 说明书在
* `apps/web/src/runtime/visual-style-catalog.ts` 文件头。
*
* 这个函数**在本次改动之前就已经没有任何调用点**(全仓搜 `renderDirectionFormBody`
* 只搜得到定义),设计风格选择题从提示词整题下线之后更不会有。留着不删是因为
* 产品明说「后续可能要找回」,而它是那条路上现成的一块。
*
* ⚠️ 同文件里**读答案**那一半(`od tools directions` / `catalogue identity` 那段)
* **是活的,别一起清掉**:旧表单交上来的 `value` / `foundation` / `guidance`
* 仍要读得懂。撤的是**发问**,不是**读答案**。
*
* Render the direction-picker form body for emission as a `<question-form>`.
* Uses the `direction-cards` question type so the UI renders each option
* as a rich card (palette swatches + type sample + mood blurb + refs)
+4 -8
View File
@@ -30,8 +30,7 @@ Three hard rules govern every new design task. They are not optional. The user i
Active design system exception: if a later section in this same system prompt is titled \`## Active design system\`, the user has already selected the brand and visual direction. In that case:
- Treat the active design system's palette, typography, spacing, and component rules as the visual direction.
- Do not ask the user to pick a separate theme color, visual direction, palette, typography mood, or direction card.
- Do not emit a direction question-form or any \`direction-cards\` question for this project.
- Do not ask the user to pick a separate theme color, visual direction, palette, or typography mood.
- In any discovery form, drop brand/direction/theme-color questions unless the user explicitly asks to switch away from the active design system.
- If an older discovery answer says \`brand: "Pick a direction for me"\`, ignore Branch A and proceed to RULE 3 using the active design system.
@@ -55,8 +54,6 @@ When the Active plugin / Active skill is \`od-default\` or "Default design route
"options": ["Slide deck / pitch", "Single web prototype / landing", "Multi-screen app prototype", "Dashboard / tool UI", "Editorial / marketing page"] },
{ "id": "audience", "label": "Who is this for?", "type": "text",
"placeholder": "e.g. early-stage investors, dev-tools buyers, internal exec review" },
{ "id": "tone", "label": "Visual tone", "type": "radio",
"options": ["Editorial / magazine", "Modern minimal", "Playful / illustrative", "Tech / utility", "Luxury / refined", "Brutalist / experimental", "Human / approachable"] },
{ "id": "brand", "label": "Brand context", "type": "radio", "default": "pick_direction",
"options": [
{ "label": "Pick a direction for me", "value": "pick_direction" },
@@ -72,8 +69,7 @@ When the Active plugin / Active skill is \`od-default\` or "Default design route
Form authoring rules:
- Body must be valid JSON. No comments. No trailing commas.
- \`type\` is one of: \`radio\`, \`checkbox\`, \`select\`, \`text\`, \`textarea\`, \`number\`, \`range\`, \`date\`, \`time\`, \`datetime-local\`, \`color\`, \`url\`, \`email\`, \`tel\`, \`file\`, \`switch\`, \`direction-cards\`.
- \`direction-cards\` is a trigger for Open Design's host-owned visual-style catalog. Emit only the question's stable \`id\`, localized \`label\`, \`type: "direction-cards"\`, and \`required\` when appropriate; omit \`options\`, \`cards\`, \`variant\`, and \`defaultValue\`. The host selects the versioned catalog, preview images, recommendation, and stable style ids from the project kind. Do not spend output tokens inventing card metadata or preview assets.
- \`type\` is one of: \`radio\`, \`checkbox\`, \`select\`, \`text\`, \`textarea\`, \`number\`, \`range\`, \`date\`, \`time\`, \`datetime-local\`, \`color\`, \`url\`, \`email\`, \`tel\`, \`file\`, \`switch\`.
- Use the most expressive mainstream web form control for the information you need: sliders for numeric intensity, color for brand/accent picks, date/time for deadlines, url/email/tel for contact/reference fields, file for upload requests, switch for binary preferences, and textarea only for genuinely open prose.
- At most 6-7 options per question; merge near-duplicates instead of listing more.
- Choose \`radio\` vs \`select\` by option count, not importance: \`radio\` for a short list, \`select\` once it runs long (languages, timezones, voices). \`checkbox\` is always a plain list.
@@ -83,8 +79,8 @@ Form authoring rules:
- When the selected or likely output is a slide deck / pitch deck, include a \`speakerNotes\` switch with \`defaultValue: true\` unless project metadata or plugin inputs already supply \`speakerNotes\`.
- For reference images, brand specs, PDFs, slide/docs, screenshots, source exports, or any brief that asks the user to "upload/paste a file", include a \`type: "file"\` question in the same form instead of asking in prose after the form. Use \`multiple: true\` when several assets are useful, and \`accept\` such as \`"image/*"\`, \`".pdf,.doc,.docx"\`, or a comma-separated mix when the needed source type is known. Selected files are uploaded into Design Files and submitted as attached/context files on the answer turn.
- For \`checkbox\` questions, include \`maxSelections\` when the user should choose only a limited number of options. Do not encode limits only in the label text.
- The host automatically renders a localized "Other" escape hatch (a chip that expands into a type-in field) on every finite-choice question EXCEPT the visual ones (\`radio\`, \`checkbox\`, \`select\`) — do NOT author your own catch-all "Other …" / "I'll describe" option there; it would duplicate the host's. A \`direction-cards\` question, and a \`tone\` question in a project that has a visual style, render as the built-in style catalog, which has no type-in field: the delivered design draws none, so the host cannot promise one. State an out in the question text if the user needs one. Leave \`allowCustom\` unset or \`true\`; add localized \`customLabel\` / \`customPlaceholder\` when the default copy is not specific enough. Only set \`allowCustom: false\` when the downstream system truly requires one exact machine id.
- Prefill every non-visual question with a recommended \`default\` inferred from the brief, project metadata, and plugin inputs — an option \`value\` for \`radio\`/\`select\`, an array of option \`value\`s for \`checkbox\`, or concrete suggested text for free-text fields, never placeholder filler. The goal is a form the user can submit unchanged and still get a sensible build; omit \`default\` only when no reasonable recommendation exists (e.g. a \`file\` upload). A \`direction-cards\` question is the exception: the host-owned catalog owns its recommendation, so do not invent a \`defaultValue\`. Place the \`default\` key before \`options\` in each other question object, as the example forms above do — the host renders forms token-by-token, and a \`default\` that trails a long \`options\` array reaches the user late.
- The host automatically renders a localized "Other" escape hatch (a chip that expands into a type-in field) on every finite-choice question EXCEPT the visual ones (\`radio\`, \`checkbox\`, \`select\`) — do NOT author your own catch-all "Other …" / "I'll describe" option there; it would duplicate the host's. Leave \`allowCustom\` unset or \`true\`; add localized \`customLabel\` / \`customPlaceholder\` when the default copy is not specific enough. Only set \`allowCustom: false\` when the downstream system truly requires one exact machine id.
- Prefill every non-visual question with a recommended \`default\` inferred from the brief, project metadata, and plugin inputs — an option \`value\` for \`radio\`/\`select\`, an array of option \`value\`s for \`checkbox\`, or concrete suggested text for free-text fields, never placeholder filler. The goal is a form the user can submit unchanged and still get a sensible build; omit \`default\` only when no reasonable recommendation exists (e.g. a \`file\` upload). Place the \`default\` key before \`options\` in each other question object, as the example forms above do — the host renders forms token-by-token, and a \`default\` that trails a long \`options\` array reaches the user late.
- Localize every user-facing string in the form (\`title\`, the per-question \`label\`, \`placeholder\`, and option \`label\`s) to the user's chat language — write what a native speaker would naturally say, never a word-for-word translation (the Chinese title is 快速确认 · 30秒, not the literal 快速简报). Set the top-level \`"lang"\` field to the BCP-47 tag of that language (e.g. \`"zh-CN"\`, \`"ja"\`) so the host renders its built-in controls (the "Other" chip, the custom-answer field) in the same language. \`id\`, \`type\`, option \`value\`, and the stable branch values (\`pick_direction\`, \`brand_spec\`, \`reference_match\`) MUST stay in English because later branch rules match against them.
- If you keep the \`brand\` question, its \`id\` must stay \`"brand"\`. Its three default branch values must stay exactly \`"pick_direction"\`, \`"brand_spec"\`, and \`"reference_match"\` even if you localize the labels.
- If the initial brief already includes a brand spec, brand-guide attachment, reference URL, or screenshot, you may drop the \`brand\` question as already answered, but you must still treat that provided source as Branch A below.
+2 -2
View File
@@ -665,7 +665,7 @@ const ACTIVE_DESIGN_SYSTEM_VISUAL_DIRECTION_OVERRIDE = `
Active design system exception: the active design system is the visual direction for this project. Use its DESIGN.md palette, typography, spacing, component rules, and theme tokens as the source of truth for color and mood.
- Do not ask the user to pick a separate theme color, visual direction, palette, typography mood, or direction card.
- Do not emit a direction question-form, a \`direction-cards\` picker, or any visual-direction card while an active design system is present.
- Do not emit a direction question-form while an active design system is present.
- If an earlier discovery answer asks to "Pick a direction for me", treat that as already satisfied by the active design system and continue with the plan.
- When a downstream framework mentions "active direction" or "theme tokens", bind those fields from the active design system instead of the built-in direction library.
`;
@@ -1448,7 +1448,7 @@ export function composeSystemPrompt({
// originating assistant message, and answers return as the next user message.
// Applies to every agent — question-form is UI-parsed markup, not a tool.
if (!isSlimCharterHead || isAskMode) parts.push(
"\n\n---\n\n## Structured clarification on any turn\n\nWhen clarification is materially necessary and the answer benefits from structured input, emit a `<question-form>` block instead of writing a bulleted list of options in markdown. The host renders it inline in the originating assistant message; a markdown list renders as plain text and forces the user to type a reply. Use the richest appropriate web form controls (`radio`, `checkbox`, `select`, `text`, `textarea`, `number`, `range`, `date`, `time`, `datetime-local`, `color`, `url`, `email`, `tel`, `file`, `switch`, or `direction-cards`). For a `direction-cards` question, emit only its intent fields; omit `options`, `cards`, `variant`, and `defaultValue` because the OpenDesign host supplies the project-kind visual catalog, previews, recommendation, and stable style ids. When the clarification needs reference images, source docs, screenshots, or other user files, combine a `type: \"file\"` question with the text/options in the same form; selected files are uploaded into Design Files and submitted as attached/context files on the answer turn. For every finite-choice question, keep user control by leaving `allowCustom` unset or setting it to `true`, and add localized `customLabel` / `customPlaceholder` when useful. Use free-form prose questions only when a form would add no structure. Do NOT also duplicate the form's questions as markdown text alongside it.\n\n`<question-form>` is assistant text for the OpenDesign UI, not a native tool call. If you need to clarify direction, emit the complete `<question-form>...</question-form>` block directly in the assistant message before any TodoWrite, file write/edit, Bash, or other native tool call. Do not stop after an introductory sentence such as \"先确认一下方向:\"; the same message must include the full form.\n\nAt most 6-7 options per question; merge near-duplicates instead of listing more. Choose `radio` vs `select` by option count, not importance: `radio` for a short list, `select` once it runs long (languages, timezones, voices); `checkbox` is always a plain list. `select` options may carry `group` (first group expands, the rest collapse) and `trailingLabel` (a short end-of-row code such as `ZH-CN`); both optional. Label options in the user's words, not jargon: \"Magazine-style layout\", not \"Editorial\". Reword only `label`; never change a stable `value`. Keep each `label` under ~40 characters; put anything longer in `description`.",
"\n\n---\n\n## Structured clarification on any turn\n\nWhen clarification is materially necessary and the answer benefits from structured input, emit a `<question-form>` block instead of writing a bulleted list of options in markdown. The host renders it inline in the originating assistant message; a markdown list renders as plain text and forces the user to type a reply. Use the richest appropriate web form controls (`radio`, `checkbox`, `select`, `text`, `textarea`, `number`, `range`, `date`, `time`, `datetime-local`, `color`, `url`, `email`, `tel`, `file`, `switch`). When the clarification needs reference images, source docs, screenshots, or other user files, combine a `type: \"file\"` question with the text/options in the same form; selected files are uploaded into Design Files and submitted as attached/context files on the answer turn. For every finite-choice question, keep user control by leaving `allowCustom` unset or setting it to `true`, and add localized `customLabel` / `customPlaceholder` when useful. Use free-form prose questions only when a form would add no structure. Do NOT also duplicate the form's questions as markdown text alongside it.\n\n`<question-form>` is assistant text for the OpenDesign UI, not a native tool call. If you need to clarify direction, emit the complete `<question-form>...</question-form>` block directly in the assistant message before any TodoWrite, file write/edit, Bash, or other native tool call. Do not stop after an introductory sentence such as \"先确认一下方向:\"; the same message must include the full form.\n\nAt most 6-7 options per question; merge near-duplicates instead of listing more. Choose `radio` vs `select` by option count, not importance: `radio` for a short list, `select` once it runs long (languages, timezones, voices); `checkbox` is always a plain list. `select` options may carry `group` (first group expands, the rest collapse) and `trailingLabel` (a short end-of-row code such as `ZH-CN`); both optional. Label options in the user's words, not jargon: \"Magazine-style layout\", not \"Editorial\". Reword only `label`; never change a stable `value`. Keep each `label` under ~40 characters; put anything longer in `description`.",
);
/*
@@ -14,7 +14,7 @@ exports[`composeSystemPrompt — scenario × section golden matrix > keeps the s
"clarifying-questions",
"role-marker-guard",
],
"totalChars": 69004,
"totalChars": 67528,
},
"ask-mode-full-context": {
"sections": [
@@ -36,7 +36,7 @@ exports[`composeSystemPrompt — scenario × section golden matrix > keeps the s
"clarifying-questions",
"role-marker-guard",
],
"totalChars": 21341,
"totalChars": 21083,
},
"codex-image-dispatcher": {
"sections": [
@@ -48,7 +48,7 @@ exports[`composeSystemPrompt — scenario × section golden matrix > keeps the s
"clarifying-questions",
"role-marker-guard",
],
"totalChars": 52801,
"totalChars": 52543,
},
"critique-enabled": {
"sections": [
@@ -69,7 +69,7 @@ exports[`composeSystemPrompt — scenario × section golden matrix > keeps the s
"clarifying-questions",
"role-marker-guard",
],
"totalChars": 67047,
"totalChars": 65513,
},
"deck-kind-no-skill": {
"sections": [
@@ -84,7 +84,7 @@ exports[`composeSystemPrompt — scenario × section golden matrix > keeps the s
"clarifying-questions",
"role-marker-guard",
],
"totalChars": 95755,
"totalChars": 94279,
},
"deck-kind-with-skill-seed": {
"sections": [
@@ -99,7 +99,7 @@ exports[`composeSystemPrompt — scenario × section golden matrix > keeps the s
"clarifying-questions",
"role-marker-guard",
],
"totalChars": 66517,
"totalChars": 65041,
},
"design-ds-fixture-fallback": {
"sections": [
@@ -120,7 +120,7 @@ exports[`composeSystemPrompt — scenario × section golden matrix > keeps the s
"clarifying-questions",
"role-marker-guard",
],
"totalChars": 91568,
"totalChars": 90034,
},
"design-full-stack": {
"sections": [
@@ -150,7 +150,7 @@ exports[`composeSystemPrompt — scenario × section golden matrix > keeps the s
"clarifying-questions",
"role-marker-guard",
],
"totalChars": 76378,
"totalChars": 74844,
},
"design-minimal": {
"sections": [
@@ -165,7 +165,7 @@ exports[`composeSystemPrompt — scenario × section golden matrix > keeps the s
"clarifying-questions",
"role-marker-guard",
],
"totalChars": 95711,
"totalChars": 94235,
},
"design-no-ds-multitarget": {
"sections": [
@@ -180,7 +180,7 @@ exports[`composeSystemPrompt — scenario × section golden matrix > keeps the s
"clarifying-questions",
"role-marker-guard",
],
"totalChars": 72083,
"totalChars": 70607,
},
"example-prompt": {
"sections": [
@@ -195,7 +195,7 @@ exports[`composeSystemPrompt — scenario × section golden matrix > keeps the s
"clarifying-questions",
"role-marker-guard",
],
"totalChars": 70330,
"totalChars": 68854,
},
"freeform-other": {
"sections": [
@@ -211,7 +211,7 @@ exports[`composeSystemPrompt — scenario × section golden matrix > keeps the s
"clarifying-questions",
"role-marker-guard",
],
"totalChars": 99407,
"totalChars": 97931,
},
"freeform-other-no-deck-signal": {
"sections": [
@@ -225,7 +225,7 @@ exports[`composeSystemPrompt — scenario × section golden matrix > keeps the s
"clarifying-questions",
"role-marker-guard",
],
"totalChars": 69253,
"totalChars": 67777,
},
"media-image": {
"sections": [
@@ -237,7 +237,7 @@ exports[`composeSystemPrompt — scenario × section golden matrix > keeps the s
"clarifying-questions",
"role-marker-guard",
],
"totalChars": 52799,
"totalChars": 52541,
},
"memory-hooks-off": {
"sections": [
@@ -254,7 +254,7 @@ exports[`composeSystemPrompt — scenario × section golden matrix > keeps the s
"clarifying-questions",
"role-marker-guard",
],
"totalChars": 97939,
"totalChars": 96463,
},
"plan-mode": {
"sections": [
@@ -269,7 +269,7 @@ exports[`composeSystemPrompt — scenario × section golden matrix > keeps the s
"clarifying-questions",
"role-marker-guard",
],
"totalChars": 73277,
"totalChars": 71801,
},
"plugin-stages": {
"sections": [
@@ -286,7 +286,7 @@ exports[`composeSystemPrompt — scenario × section golden matrix > keeps the s
"clarifying-questions",
"role-marker-guard",
],
"totalChars": 95807,
"totalChars": 94331,
},
"skip-discovery-brief": {
"sections": [
@@ -301,7 +301,7 @@ exports[`composeSystemPrompt — scenario × section golden matrix > keeps the s
"clarifying-questions",
"role-marker-guard",
],
"totalChars": 69841,
"totalChars": 68365,
},
"slim-design-full-stack": {
"sections": [
@@ -328,7 +328,7 @@ exports[`composeSystemPrompt — scenario × section golden matrix > keeps the s
"media-dispatch-hint",
"role-marker-guard",
],
"totalChars": 50693,
"totalChars": 50040,
},
"slim-freeform-no-deck-signal": {
"sections": [
@@ -338,7 +338,7 @@ exports[`composeSystemPrompt — scenario × section golden matrix > keeps the s
"media-dispatch-hint",
"role-marker-guard",
],
"totalChars": 41633,
"totalChars": 40980,
},
}
`;
+43 -13
View File
@@ -108,18 +108,27 @@ describe('renderSlimCoreCharter — frozen protocol markers', () => {
for (const value of ['pick_direction', 'brand_spec', 'reference_match']) {
expect(charter).toContain(`\`${value}\``);
}
for (const control of ['direction-cards', 'datetime-local', 'switch']) {
for (const control of ['datetime-local', 'switch']) {
expect(charter).toContain(control);
}
expect(charter).toContain('allowCustom');
});
it('requires recommended defaults except for the host-owned visual catalog', () => {
/**
* T69(2026-09-07):设计风格选择题从提示词整题下线,产品逐字「**不问了**」。
* 原用例断言的是这份 charter **教** `direction-cards` 怎么用,现在反过来守它不再教。
* 渲染器那一路仍然认得这个类型(休眠件的安全网),两边**故意不相等** ——
* 判据写在 `e2e/tests/question-form-type-parity.test.ts` 的 `DORMANT_TYPES`。
*/
it('不再向模型提供设计风格选择题', () => {
expect(charter).not.toContain('direction-cards');
expect(charter).not.toContain('visual-style catalog');
});
it('requires recommended defaults', () => {
expect(charter).toContain('provide a sensible default for each non-visual question');
expect(charter).toContain('Use `defaultValue` to preselect an answer');
expect(charter).toContain("`defaultValue` must match an option's `value`");
expect(charter).toContain('A host-owned `direction-cards` question is the exception');
expect(charter).toContain('Omit `options`, `cards`, `variant`, and `defaultValue`');
});
it('localizes user-visible form copy while preserving machine identifiers', () => {
@@ -367,7 +376,18 @@ describe('composeSystemPrompt — promptCoreVariant switch', () => {
}
});
it('keeps the injected direction-picker atom explicitly opt-in', () => {
/**
* T69(2026-09-07):`direction-picker` atom 不再提供选择器,改成**自己定方向**。
*
* 这个 atom 是这次下线里最容易漏的一处 —— 它不在
* `e2e/tests/question-form-type-parity.test.ts` 那份六条路清单里,却被
* `od-default`(默认设计路由)等五个官方场景挂在 `plan` 阶段整段拼进系统提示词,
* 正是本用例在证明的那件事。只改那六条、留着它,默认路由照旧会教模型出方向卡。
*
* 原用例守的是「这个 atom 只在用户明确要求时才弹选择器」;产品裁决之后
* **连"明确要求"这一档也没有了**,所以断言换成:它教的是怎么定方向,不是怎么问。
*/
it('注入的 direction-picker atom 自己定方向,不再问用户', () => {
const directionAtom = readFileSync(
path.join(repoRoot, 'plugins/_official/atoms/direction-picker/SKILL.md'),
'utf8',
@@ -385,16 +405,26 @@ describe('composeSystemPrompt — promptCoreVariant switch', () => {
activeStageBlocks: [stageBlock],
});
// 防真空:atom 的正文确实拼进来了,否则底下那几条 `not.toContain` 会因为
// 「整段根本没出现」而集体假绿
expect(out).toContain('# Direction picker');
expect(out).toContain('**Do not ask the user to choose a visual direction.**');
expect(out).toContain(
'The presence of this atom or the `plan` stage does not trigger a picker',
'Asking the user to pick, compare, or confirm a visual direction.',
);
expect(out).toContain('Do not\nemit direction cards proactively');
expect(out).toContain(
'When the user has not explicitly requested\noptions, infer a fitting direction',
);
expect(out).toContain('`direction-cards` is a Host-owned catalog trigger');
expect(out).toContain('Omit `options`,\n`cards`, `variant`, and `defaultValue`');
expect(out).not.toContain(
// 三条解析顺序还在:设计系统 → 用户给的品牌源 → 自己推断
expect(out).toContain('An active design system');
expect(out).toContain('infer the best-matching direction yourself');
/* 否定断言只对着 **atom 正文**,不是整份系统提示词 —— 后者当然还会讲
`question-form`(那是别的题型的合法用法),对着 `out` 断言会永远红。
只钉 `direction-cards` 这一个名字。atom 里那句「不要用 question-form 问方向」
**是要留的**:`<question-form>` 本来就是模型在别处学过的通用能力,
这里点它的名是在**划范围**,不是在泄露一个本该藏起来的能力 ——
和 `direction-cards` 不同,后者除了问设计风格没有第二种用途。 */
expect(directionAtom).not.toContain('direction-cards');
expect(directionAtom).not.toContain(
'The direction-picker atom asks the agent to draft',
);
});
@@ -46,16 +46,22 @@ describe('discovery.ts — on-demand clarification policy', () => {
expect(DISCOVERY_AND_PHILOSOPHY).toContain('**Hard cap: 5 questions per form — never more.**');
});
it('treats direction-cards as a host-owned catalog trigger', () => {
expect(DISCOVERY_AND_PHILOSOPHY).toContain(
"`direction-cards` is a trigger for Open Design's host-owned visual-style catalog",
);
expect(DISCOVERY_AND_PHILOSOPHY).toContain(
'omit `options`, `cards`, `variant`, and `defaultValue`',
);
expect(DISCOVERY_AND_PHILOSOPHY).toContain(
'the host-owned catalog owns its recommendation',
);
/**
* T69(2026-09-07):设计风格选择题从提示词整题下线,产品逐字「**不问了**」。
*
* 原用例守的是「`direction-cards` 是 host 目录触发器」这套用法说明。现在反过来:
* 开场简报里**两个入口**都要没了 —— 明面上的 `direction-cards`,和那道长得像
* 普通单选、却被 `QuestionForm.tsx` 的 `asksVisualDirection` 认走换成整份目录的
* `tone`。只撤前者会留下后者这条更隐蔽的路。
*/
it('开场简报不再提供任何一条问设计风格的路', () => {
expect(DISCOVERY_AND_PHILOSOPHY).not.toContain('direction-cards');
expect(DISCOVERY_AND_PHILOSOPHY).not.toContain('visual-style catalog');
expect(DISCOVERY_AND_PHILOSOPHY).not.toMatch(/"id":\s*"tone"/);
// 防真空:示例简报本身还在,别的题一道没少
expect(DISCOVERY_AND_PHILOSOPHY).toContain('"id": "output"');
expect(DISCOVERY_AND_PHILOSOPHY).toContain('"id": "brand"');
expect(DISCOVERY_AND_PHILOSOPHY).toContain('"id": "scale"');
});
it('leaves the task-type form to od-default while accepting historical answers', () => {
@@ -108,7 +108,10 @@ describe('active skill clarification policy', () => {
},
{
path: 'plugins/_official/atoms/direction-picker/SKILL.md',
required: 'Do not\nemit direction cards proactively',
/* T69(2026-09-07):这个 atom 从「只在用户明确要求时才弹选择器」改成
**完全不问**。本行守的仍是同一件事(别把澄清变成固定关卡),
只是判据句跟着 atom 的新正文走。 */
required: '**Do not ask the user to choose a visual direction.**',
forbidden: ['lets the user choose before final generation'],
},
{
+10 -7
View File
@@ -488,20 +488,23 @@ describe('composeSystemPrompt', () => {
const prompt = composeSystemPrompt({ agentId: 'amr' });
expect(prompt).toContain('## Structured clarification on any turn');
expect(prompt).toContain('`<question-form>` is assistant text for the OpenDesign UI, not a native tool call');
expect(prompt).toContain(
'For a `direction-cards` question, emit only its intent fields; omit `options`, `cards`, `variant`, and `defaultValue`',
);
expect(prompt).toContain(
'emit the complete `<question-form>...</question-form>` block directly in the assistant message before any TodoWrite, file write/edit, Bash, or other native tool call',
);
expect(prompt).toContain('Do not stop after an introductory sentence such as "先确认一下方向:"');
});
it('keeps the host-owned direction-card boundary in bare Ask mode', () => {
/**
* T69(2026-09-07):设计风格选择题从提示词整题下线,产品逐字「**不问了**」。
* 原用例守的是「裸 Ask 模式里也要保留 host 目录那条边界说明」——
* 那条说明本身就是在**教模型这个能力存在**,现在连它一起撤。
*/
it('裸 Ask 模式里也不再提设计风格选择题', () => {
const prompt = composeSystemPrompt({ sessionMode: 'chat' });
expect(prompt).toContain(
'the OpenDesign host supplies the project-kind visual catalog, previews, recommendation, and stable style ids',
);
// 防真空:那一整段结构化澄清的授权还在,不是因为整段没composed 才绿
expect(prompt).toContain('## Structured clarification on any turn');
expect(prompt).not.toContain('direction-cards');
expect(prompt).not.toContain('project-kind visual catalog');
});
it('pins filesystem artifact handoff for other CLI agents too', () => {
@@ -1,25 +1,39 @@
/**
* 视觉调性是**单选**。
* 视觉调性那道题**已整题下线**(T69,2026-09-07)。
*
* 用户裁决(2026-08-27):「就是要单选啊,为啥要选两个风格? …最终 html 只会有
* 一种风格才对吧? 除非我强制要 agent 把两个风格融合,不然默认都应该是一个风格」。
* ⚠️ 文件名还叫 `tone-single-select` 是**故意的** —— 这里是那条裁决链的落点,
* 改名会让「当年为什么定成单选、后来为什么整题没了」这段线索断掉。
*
* 支撑这条的调查(subagent,同日):
* · 代码里**没有任何地方硬编码 2** —— 解析与执行都读模型给的数;那个 2 只活在
* 提示词的示例里,而模型在 10 个真实表单里三次发出调性题,**三次都照抄了 2**。
* · 下游**没有任何东西融合两个调性**:两个值只是被 `formatFormAnswers` 用逗号
* 拼成一行散文;daemon 从不按题目 id 解析正文;所有提示词都是单数的
* (「Pick **a direction**」/「Choose the **best-matching** option」),
* 唯一相关的明文规则还是反面的(`design-templates/replit-deck/references/themes.md:22`
* 「Never mix two themes in one deck」)。
* · 后果:界面承诺了一对,而没有任何代码或提示词把它当成一对用 —— 实际上
* 有一个会悄悄胜出。
* ── 现在的裁决(2026-09-07,产品逐字)──────────────────────────
*
* 顺带修掉一个真实死路:预填正好填满 `maxSelections`,于是这一题**一打开就到上限**,
* 之后每次新点击都是「你得先自己发现要取消一个」。改成单选后不存在这个状态。
* 「选中态就是当前切换到的那个效果,或者你能否把提示词里让 agent 感知到
* question-form 能出设计风格的那些提示词下掉?**不问了**,这些代码先讲提示词
* 干掉,组件代码注释,后续可能要找回」
*
* 两份提示词是**镜像**的(daemon 一份、contracts 一份给 BYOK 用),必须一起改 ——
* 只改一份会让两条通路给出不同的表单。
* 于是开场简报里那道 `{ "id": "tone", "label": "Visual tone", "type": "radio" }`
* 整条撤掉。它是设计风格选择卡的**第二个入口**,而且比 `direction-cards` 隐蔽 ——
* 它长得像一道普通单选,渲染时却被 `QuestionForm.tsx` 的 `asksVisualDirection`
* (`q.id === 'tone'`)认走,换成整份风格目录。只撤 `direction-cards` 会留下它。
*
* ── 被这条推翻的旧裁决 ───────────────────────────────────────
*
* **2026-08-27 用户裁决**(原文):「就是要单选啊,为啥要选两个风格? …最终 html
* 只会有一种风格才对吧? 除非我强制要 agent 把两个风格融合,不然默认都应该是
* 一个风格」。当时的调查支撑(同日 subagent)如下,**结论本身没有被证伪**,
* 只是它守的那道题不存在了:
* · 代码里没有任何地方硬编码 2 —— 那个 2 只活在提示词的示例里,而模型在 10 个
* 真实表单里三次发出调性题,三次都照抄了 2;
* · 下游没有任何东西融合两个调性:两个值只是被 `formatFormAnswers` 拼成一行散文;
* · 后果是界面承诺了一对,却没有代码把它当成一对用 —— 实际有一个会悄悄胜出。
*
* 原文件里那条 `选项还在 —— 别把这题整个删了` 是**防误删**的守卫。这次的删除
* **不是误删,是产品指令**,所以它连同其余调性断言一起退场,换成下面的反向守卫。
*
* ── 这里还留着什么 ───────────────────────────────────────────
*
* 一条正向守卫(`maxSelections` 这个能力本身别跟着一起被扫掉)和一条反向守卫
* (调性题别被谁"顺手加回来")。提示词那七条路撤干净没有,由
* `e2e/tests/question-form-visual-style-retired.test.ts` 正面守着。
*/
import { describe, expect, it } from 'vitest';
import { readFileSync } from 'node:fs';
@@ -33,36 +47,47 @@ const contractsPrompt = readFileSync(
'utf8',
);
/** 取调性那一题的示例 JSON 片段 */
function toneExample(src: string): string {
const i = src.indexOf('"id": "tone"');
if (i < 0) throw new Error('tone example not found — 改名了,断言会空转');
return src.slice(i, src.indexOf('},', i));
}
const MIRRORS = [
['daemon', daemonPrompt],
['contracts', contractsPrompt],
] as const;
describe('调性题是单选', () => {
for (const [name, src] of [['daemon', daemonPrompt], ['contracts', contractsPrompt]] as const) {
it(`${name}:示例用 radio,不是 checkbox`, () => {
expect(toneExample(src)).toMatch(/"type":\s*"radio"/);
describe('调性题已整题下线(T69)', () => {
for (const [name, src] of MIRRORS) {
it(`${name}:开场简报的示例表单里没有调性题`, () => {
expect(src).not.toMatch(/"id":\s*"tone"/);
// 「Visual tone」这个标签也不许以别的 id 换皮回来
expect(src).not.toContain('Visual tone');
});
it(`${name}:示例不再带 maxSelections`, () => {
expect(toneExample(src)).not.toMatch(/maxSelections/);
});
it(`${name}:选项还在 —— 别把这题整个删了`, () => {
expect(toneExample(src)).toMatch(/"options"/);
expect(toneExample(src)).toMatch(/Modern minimal/);
it(`${name}:防真空 —— 示例表单本身还在,别的题一道没少`, () => {
/* 少了这条,上面那条会因为「整个示例表单都没了」而假绿。
四道题是撤掉调性之后应有的全部:做什么 / 给谁 / 品牌 / 多大体量。 */
expect(src).toContain('<question-form id="discovery"');
for (const id of ['output', 'audience', 'brand', 'scale']) {
expect(src, `${id} 那道题不见了 —— 这次只该撤调性`).toMatch(
new RegExp(`"id":\\s*"${id}"`),
);
}
});
}
it('maxSelections 这个能力本身保留 —— 别的题可能真需要限量', () => {
/* 这一条和调性题正交:它当年是顺带记下来的,今天仍然成立。
调性题曾是唯一用到 `maxSelections` 的示例,撤题时很容易把规则一起扫掉。 */
expect(daemonPrompt).toMatch(/maxSelections/);
expect(contractsPrompt).toMatch(/maxSelections/);
});
it('两份提示词的调性题保持一致 —— 它们是镜像', () => {
const strip = (s: string) => s.replace(/\s+/g, ' ').trim();
expect(strip(toneExample(daemonPrompt))).toBe(strip(toneExample(contractsPrompt)));
it('两份提示词仍然是镜像 —— 只改一份会让两条通路给出不同的表单', () => {
/* 原文件用「调性题那一段逐字相等」来守镜像关系。那一段没了,改用整份示例
表单相等 —— 守的是同一件事,而且覆盖面更大。 */
const formOf = (src: string): string => {
const start = src.indexOf('<question-form id="discovery"');
const end = src.indexOf('</question-form>', start);
if (start < 0 || end < 0) throw new Error('示例表单找不到了 —— 断言会空转');
return src.slice(start, end).replace(/\s+/g, ' ').trim();
};
expect(formOf(daemonPrompt)).toBe(formOf(contractsPrompt));
});
});
@@ -1326,6 +1326,110 @@ ${question}`),
expect(persisted?.blockedContext).toBeUndefined();
});
/**
* RED SPEC — reproduces the field failure recorded on Open Design Beta
* 0.21.1-beta.7, task `odnext_c4ee010be6b748dc9b92984946bc10a8`,
* run `e5d6181b-1705-4a44-964b-cdcb3fbcb6ac`.
*
* The user asked, in an OD Next prototype project:
* 「详细讲讲这个页面的实现思路,分十节展开,每节写满一段。
* 只输出文字,不要创建或修改文件。」
* The agent obeyed: 2940 characters of prose, no question form, no machine
* block, no file touched. The child process exited 0 and the daemon persisted
* the Run as `succeeded` with `errorCode: null` and `artifactCount: 0`.
*
* The task nevertheless landed terminal-`blocked` on
* `od_next_protocol_runtime_state_missing`, and the web client remapped the
* succeeded Run to `failed`, so a fully answered question was presented to
* the user as a task failure.
*
* Fixture shape is taken from that record, not invented: the route is still
* unlocked (production calls `prepareStrategyIntake`, never
* `prepareStrategyRequest`, on the request turn — routes.ts:2808), the
* clarification budget is untouched, and the completion evidence is what
* `validateRunDeliverable` resolves for a Run that wrote nothing.
*/
it('does not fail a request turn whose only output was the answer the user asked for', () => {
prepareStrategyIntake(db, {
taskExecutionId: 'task-1',
intake: intakePassed,
execution: executionPassed,
});
const proseOnlyAnswer = [
'这份页面的实现思路,分十节讲。',
'',
'**一、单文件架构与可编辑性**',
'整页收敛在一个 HTML 文件里,样式与脚本内联,便于整体替换。',
'',
'**二、版式栅格**',
'主栏与侧注共用一套基线网格,行高按字号的整数倍对齐。',
].join('\n');
const result = finalizeStrategyPlanningTurn(db, {
taskExecutionId: 'task-1',
runId: 'run-request',
protocol: protocol(proseOnlyAnswer),
// The process succeeded; nothing was written, because nothing was asked
// to be written.
completionEvidence: { physicalStatus: 'succeeded', deliverableValid: false },
updatedAt: 120,
});
expect(result.action).not.toBe('blocked');
const persisted = getStrategyTaskExecution(db, 'task-1');
expect(persisted?.outcome).not.toBe('blocked');
expect(persisted?.blockedContext).toBeUndefined();
});
/**
* Companion evidence for the spec above — expected to PASS today.
*
* It establishes that the block is not the agent misbehaving. Enumerate every
* Runtime State the schema admits for an unrouted request turn and feed each
* one to the same prose-only turn: all of them are refused too. There is no
* declaration the agent could have emitted that would have let a
* deliverable-free answer through, so "the reply did not carry the
* machine-readable state" describes a contract with no legal move, not a
* protocol violation.
*/
it('admits no request-stage runtime state for a turn that delivers nothing', () => {
const declarable = [
runtimeState({ route: 'full_plan', outcome: 'clarification_required' }),
runtimeState({ route: 'full_plan', outcome: 'plan_ready', executionMode: 'simple' }),
runtimeState({ route: 'direct_edit', outcome: 'completed', executionMode: 'simple' }),
];
const refusals = declarable.map((state, index) => {
const taskExecutionId = `task-declared-${index}`;
createStrategyTaskExecution(db, {
taskExecutionId,
projectId: 'project-1',
conversationId: 'conversation-1',
snapshotId: snapshot.snapshotId,
selectedAgentId: AGENT_ID,
initialRunId: `run-declared-${index}`,
...strategyTaskCreateIdentityFixture(),
createdAt: 100,
});
prepareStrategyIntake(db, {
taskExecutionId,
intake: intakePassed,
execution: executionPassed,
});
const outcome = finalizeStrategyPlanningTurn(db, {
taskExecutionId,
runId: `run-declared-${index}`,
protocol: protocol(`答案正文。\n${block('open-design-runtime-state', state)}`),
completionEvidence: { physicalStatus: 'succeeded', deliverableValid: false },
updatedAt: 120,
});
return { declared: state.outcome, action: outcome.action };
});
expect(refusals).toEqual([
{ declared: 'clarification_required', action: 'blocked' },
{ declared: 'plan_ready', action: 'blocked' },
{ declared: 'completed', action: 'blocked' },
]);
});
it('accepts a clarification turn whose state predicted a premature execution mode', () => {
prepareStrategyRequest(db, {
taskExecutionId: 'task-1', preference: 'full_plan', directEdit: directEligible,
+56 -3
View File
@@ -578,7 +578,7 @@ export const QuestionFormView = forwardRef<QuestionFormHandle, Props>(function Q
// "(skipped)",让 agent 拿默认值往下走。判据来自交付稿意图澄清那五格的状态标签
// (5-1「一个都没选 ——『下一步』置灰」/ 5-4「没写字前『下一步』仍置灰」)。
const requiredAnswered = form.questions.every((q) => {
if (!questionNeedsAnswer(q)) return true;
if (!questionNeedsAnswer(q, visualStyleContext)) return true;
if (skippedQuestionIds.has(q.id)) return true;
const v = currentAnswers[q.id];
return questionAnswerIsPresent(v);
@@ -646,7 +646,7 @@ export const QuestionFormView = forwardRef<QuestionFormHandle, Props>(function Q
if (!activeQuestion) return true;
// 分步态下「下一步」也不许在半截的 Hex 上放行
if (colorTextIsInvalid(activeQuestion.id)) return false;
if (!questionNeedsAnswer(activeQuestion)) return true;
if (!questionNeedsAnswer(activeQuestion, visualStyleContext)) return true;
if (skippedQuestionIds.has(activeQuestion.id)) return true;
return questionAnswerIsPresent(currentAnswers[activeQuestion.id]);
})();
@@ -1399,6 +1399,18 @@ type VisualStyleView = 'fan' | 'grid';
* 所以这里按稿子实现,不自造一个稿子上没有的输入位。
*/
/**
* ⚠️ **休眠件(T69,2026-09-07)** —— 说明书在 `runtime/visual-style-catalog.ts`
* 文件头。设计风格选择题已从提示词整题下线(产品逐字「不问了 …… 组件代码注释,
* 后续可能要找回」),所以**正常流程里没有上游会再触发这个组件**。
*
* 代码保持可用是**有意的**:它同时是安全网 —— 缓存的旧提示词 / 旧客户端 / 模型
* 记住的旧格式若仍发来 `direction-cards` 或 `tone`,这里照旧渲染出完整的选择卡,
* 而不是一块空白。以下同族组件都属于这一批:`VisualDirectionStack`、
* `VisualDirectionCardView`、`VisualStylePreview`、`DirectionCardsPicker`。
*
* **不要**因为「线上看不到它」就删控件、删测试、或把裁决注释清理掉。
*/
function VisualStylePicker({
cards,
context,
@@ -2332,7 +2344,48 @@ function formWithVisualStyleOptions(
* 稿子没画过那种卡,不该顺手把它也收紧。`required` 仍然独立成立。
*/
const CHOICE_QUESTION_TYPES = new Set(['radio', 'checkbox', 'direction-cards']);
function questionNeedsAnswer(q: QuestionForm['questions'][number]): boolean {
/**
* 这道题**一个可点的东西都渲染不出来**。
*
* 只有 `direction-cards` 会落到这里,因为它是唯一一个**自己不带选项**的选择题:
* 素材要么来自 host 目录(前提是项目有 `visualStyleContext`),要么来自模型自带的
* 老式 `cards`。两条都没有时,渲染那两条分支
* (`visualStyleCards && visualStyleContext` / `q.cards && q.cards.length > 0`)
* 全都不成立 —— 屏幕上只剩一个标题。`options` 救不了它:`direction-cards` 没有
* 任何一条渲染分支读 `options`。
*
* ⚠️ 这个谓词是上面那两条渲染条件的**镜像**,改任何一边都要改另一边;
* `tests/components/question-form-direction-cards-dead-end.test.tsx` 的
* 「前提成立」与「对照组」两条用例就是钉这个对应关系的。
*/
function questionRendersNoChoices(
q: QuestionForm['questions'][number],
visualStyleContext: VisualStyleContext | undefined,
): boolean {
if (q.type !== 'direction-cards') return false;
if (visualStyleContext !== undefined) return false; // host 目录接管,有整份目录可点
return !(q.cards && q.cards.length > 0);
}
/**
* 这道题算不算「必须先有答案才放行」。
*
* 判据来自交付稿意图澄清那五格(5-1「一个都没选 ——「下一步」置灰」),
* 所以有选项的问题一律必答,不看 `required`。
*
* **唯一的例外是渲染不出任何选项的题**:它挡住「下一步」就成了一条死路 ——
* 用户面对一道空题,既无从作答,又永远点不亮提交,整张表只剩「跳过」。
* 这在 2026-09-07 把设计风格题从提示词整题下线之后更要紧:`direction-cards`
* 从此是个**不再被宣传的类型**,它的每一次出现都是计划外的(缓存的旧提示词、
* 旧客户端、模型记住的旧格式),也就更可能缺素材。
* 这条例外压过 `required` —— 模型标不标必答,都改变不了「这道题没东西可点」。
*/
function questionNeedsAnswer(
q: QuestionForm['questions'][number],
visualStyleContext: VisualStyleContext | undefined,
): boolean {
if (questionRendersNoChoices(q, visualStyleContext)) return false;
return q.required === true || CHOICE_QUESTION_TYPES.has(q.type);
}
@@ -1,3 +1,58 @@
/**
* ⚠️ **休眠件 —— 设计风格选择这一整套的说明书,后来人先读这一段。**
*
* 这份目录、`visual-style-deck.ts`、以及 `components/QuestionForm.tsx` 里的
* `VisualStylePicker` / `VisualDirectionStack` / `VisualDirectionCardView` /
* `VisualStylePreview` / `DirectionCardsPicker`,合起来是「看图选设计风格」那张卡。
* 代码全都**原样活着、随时能跑**,只是**没有上游会再触发它**。
*
* ── 为什么现在不可达(T69,2026-09-07)────────────────────────
*
* 产品裁决,逐字:
*
* 「选中态就是当前切换到的那个效果,或者你能否把提示词里让 agent 感知到
* question-form 能出设计风格的那些提示词下掉?**不问了**,这些代码先讲提示词
* 干掉,**组件代码注释,后续可能要找回**」
*
* 于是断的是**源头**,不是渲染层:七条提示词路径里,让模型知道自己能出设计风格题
* 的话全部撤掉(`direction-cards` 这个类型 + 开场简报里那道 `tone`)。模型不再发,
* 这张卡自然不再出现。**渲染路径一行没删** —— 详见下面「安全网」。
*
* 这是**对交付稿的有意偏离**:交付稿 `729fa43ce7` 的 `cmp-clarify` 第 21 / 22 格
* 画的就是这张卡,状态标签逐字写着「选中一张 · 图上落绿勾,「下一步」才亮起」。
* 别当成漏做补回去。裁决全文见 `specs/current/chat-panel-decisions-sheet.md` 的 T69。
*
* ── 安全网:它为什么必须继续能渲染 ───────────────────────────
*
* 提示词撤了,不等于线上不会再来:缓存的旧提示词、旧版客户端、模型自己记住的旧
* 格式,都还可能发来一份 `direction-cards` 表单。渲染器因此**继续认这个类型**
* (`artifacts/question-form.ts` 的 `QuestionType` 联合类型里它还在),
* 否则那道题会变成一块只有标题的空白。
* 提示词与渲染器**故意不相等**这件事,判据写在
* `e2e/tests/question-form-type-parity.test.ts` 的 `DORMANT_TYPES`。
*
* 顺带:`prompts/directions.ts` 里**读答案**那半边也故意留着 —— 旧表单交上来的
* `value` / `foundation` / `guidance` 仍要读得懂。撤的是**发问**,不是**读答案**。
*
* ── 要找回来,动这几处就够 ───────────────────────────────────
*
* 1. 提示词七处放回去(六条 question-form 授权路径 + `direction-picker` atom):
* 类型清单里的 `direction-cards`、它的作者规则、以及开场简报示例里那道
* `{ "id": "tone", "type": "radio", … }`。七处一起,少一处就只有部分路径会发。
* 2. `e2e/tests/question-form-type-parity.test.ts` 的 `DORMANT_TYPES` 清空,
* 判据从「渲染器 − 休眠集」变回集合相等。
* 3. `e2e/tests/question-form-visual-style-retired.test.ts` 整个删掉(它守的正是
* "撤干净了"),`apps/daemon/tests/prompts/tone-single-select.test.ts` 翻回正向。
* 4. UI 侧**什么都不用改** —— 控件、目录、一批四张、换一批、网格切换、勾选圈
* 全都还在原地,连测试都还绿着(见下面「测试留着」)。
*
* ── 测试留着 ─────────────────────────────────────────────────
*
* `tests/runtime/visual-style-deck.test.ts`、`tests/components/QuestionForm.deck-batch.test.tsx`、
* `tests/components/QuestionForm.direction-cards-catalog.test.tsx`、
* `tests/components/chat/w75-visual-direction-card.test.tsx` 等一律**保留**:
* 它们测的是休眠件**本身**,是找回来那天的保障,不是这次断掉的那条接线。
*/
export type VisualStyleContext = 'deck' | 'prototype' | 'document' | 'image' | 'video';
export type VisualStyleCategory = 'business' | 'editorial' | 'creative' | 'minimal';
@@ -1,4 +1,10 @@
/**
* ⚠️ **休眠件(T69,2026-09-07)** —— 说明书在 `runtime/visual-style-catalog.ts`
* 文件头,先读那一段:为什么现在不可达、安全网是什么、怎么找回来。
* 一句话版:设计风格选择题已从提示词整题下线(产品逐字「不问了」),
* 但**渲染路径一行没删**,产品明说「后续可能要找回」。下面这些裁决因此
* **全部原样保留、不要清理**。
*
* 视觉方向那一沓的**牌面** —— 每次露面的是目录里的哪 6 张。
*
* 产品口径(2026-08-27,逐字):
@@ -0,0 +1,102 @@
// @vitest-environment jsdom
/**
* 一道**渲染不出任何选项**的 `direction-cards` 不许把整张表锁死。
*
* ── 缺陷形状 ────────────────────────────────────────────────
*
* `direction-cards` 有两条素材来源,缺一条就换另一条:
* · host 自带的风格目录 —— 前提是项目有 `visualStyleContext`;
* · 模型自己带的 `cards` —— 老格式,现在的提示词明确要求省略。
*
* **两条同时没有**时,`QuestionForm.tsx` 那两处渲染分支
* (`visualStyleCards && visualStyleContext` / `q.cards && q.cards.length > 0`)
* 都不成立 —— 这道题只剩一个标题,底下什么都没有。
*
* 而 `direction-cards` 又躺在 `CHOICE_QUESTION_TYPES` 里,于是
* `questionNeedsAnswer` 说它「必须有答案」→ `requiredAnswered` 永远 false →
* 「下一步」**永远置灰**。用户看着一道空题,唯一出口只剩「跳过」。
*
* ── 什么时候真会撞上 ─────────────────────────────────────────
*
* `visualStyleContextForProjectKind`(`AssistantMessage.tsx`)对
* `audio` / `brand` / `orbit` / `design_system` 四种项目、以及**项目类型还没落定**
* (`projectKind === null`)一律返回 `undefined`。这几种项目里模型若发一道
* 不带 `cards` 的 `direction-cards`,就是上面那个死角。
*
* 这个洞**本来就在**,不是这次改出来的。但 2026-09-07 把设计风格题从提示词整题
* 下线之后,`direction-cards` 变成一个**不再被宣传的类型** —— 从此它的每一次出现
* 都是「计划外的」(缓存的旧提示词、别的客户端版本、模型记住的旧格式),
* 也就更可能是这种缺素材的畸形形态。安全网必须真的兜得住,不能只是「留着代码」。
*
* ── 这条测试证明什么 ─────────────────────────────────────────
*
* 只证明**不会把人锁死**:「下一步」仍然可点、答案照常提交。它**不**保证那道题
* 好看 —— 一道空题本来就该由上游别发出来,这里只负责不让它变成死路。
*/
import { afterEach, describe, expect, it, vi } from 'vitest';
import { cleanup, render as rtlRender, screen } from '@testing-library/react';
import type { ReactElement } from 'react';
import { QuestionFormView } from '../../src/components/QuestionForm';
import type { QuestionForm } from '../../src/artifacts/question-form';
import { I18nProvider } from '../../src/i18n';
afterEach(cleanup);
const render = (ui: ReactElement) =>
rtlRender(<I18nProvider initial="zh-CN">{ui}</I18nProvider>);
/** 底栏那颗主按钮(「下一步」)。 */
const nextBtn = (): HTMLButtonElement => {
const hit = [...document.querySelectorAll<HTMLButtonElement>('button')].find((b) =>
b.classList.contains('qf-primary-action'),
);
if (!hit) throw new Error('没有找到「下一步」');
return hit;
};
/** 提示词今天要求的形态:只有 id / label / type,没有 options、没有 cards。 */
const bare = (extra: Record<string, unknown> = {}): QuestionForm =>
({
id: 'f-dead-end',
title: '先定个方向',
questions: [
{ id: 'direction', label: '视觉方向', type: 'direction-cards', ...extra },
],
}) as unknown as QuestionForm;
describe('渲染不出选项的 direction-cards 不锁死提交', () => {
it('前提成立:这道题确实一张卡都没渲染出来', () => {
// 防真空 —— 底下三条如果是因为「其实渲染出来了」而绿,那什么都没测到
render(<QuestionFormView form={bare()} interactive onSubmit={vi.fn()} />);
expect(document.querySelector('.qf-visual-card')).toBeNull();
expect(document.querySelector('[data-testid="question-form-visual-picker"]')).toBeNull();
});
it('没有目录上下文、也没有模型自带 cards 时,「下一步」仍然可点', () => {
render(<QuestionFormView form={bare()} interactive onSubmit={vi.fn()} />);
expect(nextBtn().disabled).toBe(false);
});
it('模型把它标成 required 也一样 —— 一道空题不能当门闩', () => {
render(<QuestionFormView form={bare({ required: true })} interactive onSubmit={vi.fn()} />);
expect(nextBtn().disabled).toBe(false);
});
it('点得下去,而且真的提交了', () => {
const onSubmit = vi.fn();
render(<QuestionFormView form={bare()} interactive onSubmit={onSubmit} />);
nextBtn().click();
expect(onSubmit).toHaveBeenCalledTimes(1);
});
it('对照组:同一道题**渲染得出**卡片时,门闩照旧 —— 没选就是置灰', () => {
/*
* 这一条把上面三条的适用范围钉死:放宽只发生在「什么都渲染不出来」那一种,
* 有卡可点时交付稿 5-1「一个都没选 ——「下一步」置灰」继续生效。
* 少了它,上面三条会被误读成「direction-cards 从此不再是必答题」。
*/
render(<QuestionFormView form={bare()} interactive visualStyleContext="prototype" onSubmit={vi.fn()} />);
expect(document.querySelectorAll('.qf-visual-card').length).toBeGreaterThan(0);
expect(nextBtn().disabled).toBe(true);
});
});
+41 -2
View File
@@ -38,6 +38,30 @@ const PROMPT_PATHS: { rel: string; reachable: string }[] = [
{ rel: 'packages/contracts/src/prompts/discovery.ts', reachable: '当前无运行时消费者(镜像)' },
];
/**
* **休眠类型** —— 渲染器还认,但提示词**不再向模型提供**。
*
* ── 判据变更(2026-09-07,T69)────────────────────────────────
*
* 本文件原来断言的是「提示词的类型清单 == 渲染器的类型清单」,**集合相等**。
* 产品当天裁决把设计风格选择题整题下线(逐字:「把提示词里让 agent 感知到
* question-form 能出设计风格的那些提示词下掉?**不问了**」),同时明确
* **组件代码留着当休眠件**(「后续可能要找回」)。
*
* 这两句话合起来就要求两侧**故意不相等**:
* · 渲染器**继续**认 `direction-cards` —— 缓存的旧提示词、旧客户端、模型记住的
* 旧格式都还可能发来这种表单,认不得它那道题会渲染成一块空白;
* · 提示词**不再**提它 —— 提了就等于告诉模型「你可以问设计风格」。
*
* 所以判据从「相等」放宽成「**提示词 == 渲染器 − 休眠集**」。放宽的**只有这一格**,
* 而且写成一份显式名单:任何**别的**类型在某条路上漏掉,照旧当场红。
*
* ⚠️ 往这个集合里加名字 = 宣布又一个能力对模型不可见,**必须有产品裁决**;
* 不要拿它当「这条路提示词写漏了」的消音器。
* 撤干净没有由 `question-form-visual-style-retired.test.ts` 正面守着。
*/
const DORMANT_TYPES = new Set(['direction-cards']);
/** 事实源:web 渲染器认得的那些类型。提示词不能承诺渲染器做不到的事。 */
function supportedTypes(): Set<string> {
const src = read('apps/web/src/artifacts/question-form.ts');
@@ -46,6 +70,11 @@ function supportedTypes(): Set<string> {
return new Set([...union[1]!.matchAll(/'([a-z-]+)'/g)].map((m) => m[1]!));
}
/** 提示词**应当**枚举的那些类型:渲染器认得的,减去已休眠的。 */
function advertisedTypes(): Set<string> {
return new Set([...supportedTypes()].filter((type) => !DORMANT_TYPES.has(type)));
}
/**
* 抽出一份提示词里的类型清单。
*
@@ -82,19 +111,29 @@ describe('question-form 提示词跨路径一致性', () => {
}
});
it('六条路径都完整枚举了渲染器支持的每一个类型', () => {
it('六条路径都完整枚举了渲染器支持的、且仍在对模型开放的每一个类型', () => {
const supported = supportedTypes();
expect(supported.size, '联合类型抽空了 —— 抽取逻辑坏了,不是提示词的问题')
.toBeGreaterThan(10);
const advertised = advertisedTypes();
/* 休眠集必须真的是渲染器认得的那些类型的子集 —— 否则名单里躺着一个
早就不存在的名字,这条放宽就成了永远不会被发现的空洞。 */
for (const dormant of DORMANT_TYPES) {
expect(supported.has(dormant), `休眠名单里的 ${dormant} 渲染器已经不认了 —— 名单该清了`)
.toBe(true);
}
for (const { rel } of PROMPT_PATHS) {
/*
* 断言的是**和事实源相等**,不是「和第一条路相等」。
* 拿其中一条当基准,六条一起漏掉同一个类型时会集体绿 —— 那正是这一族
* 事故的形状(六份手抄件一起过时)。渲染器新增一个类型,六条必须都学会。
*
* 事实源今天是「渲染器 − 休眠集」(见 `DORMANT_TYPES`):**多**写一个休眠
* 类型和**少**写一个在用类型,两边都还是当场红。
*/
expect([...typeListIn(rel, supported)].sort(), `${rel} 的类型清单和渲染器对不上`)
.toEqual([...supported].sort());
.toEqual([...advertised].sort());
}
});
@@ -0,0 +1,127 @@
/**
* 「设计风格选择题」整题下线 —— 从**提示词源头**断掉,不是在渲染层拦。
*
* ── 产品裁决(2026-09-07,逐字)────────────────────────────────
*
* 「选中态就是当前切换到的那个效果,或者你能否把提示词里让 agent 感知到
* question-form 能出设计风格的那些提示词下掉?**不问了**,这些代码先讲提示词
* 干掉,组件代码注释,后续可能要找回」
*
* 也就是说:**模型不再被告知它可以出设计风格题**。渲染那一路(`VisualStylePicker`
* / `VisualDirectionStack` / `visual-style-catalog` / `visual-style-deck`)**原地留着
* 当休眠件**,产品明说「后续可能要找回」—— 所以本文件断言的是**提示词**,
* 不是「渲染器不认这个类型」。两者的分工写在 `RETIRED_FROM_PROMPTS` 那条注释里。
*
* ── 这个测试能证明什么、不能证明什么 ──────────────────────────
*
* **能证明**:七条授权路径的提示词文本里,再没有任何一处向模型**提供**
* `direction-cards` 这个类型、教它 host 目录的用法、或在示例简报里摆一道
* 视觉风格题。少改一条路就红 —— 这一族事故的形状正是「六份手抄件改漏一份」
* (见 `question-form-type-parity.test.ts` 的抬头)。
*
* **不能证明**:模型**不会**自己开一道问视觉风格的题。它完全可以自造一道
* `{ id: "tone", type: "radio", options: ["极简", "编辑感", …] }` —— 那是模型的
* 自由发挥,提示词管不住,只有真机长期观察能说话。本文件守的是「我们没教它」,
* 不是「它学不会」。
*
* **也不能证明**:线上缓存的旧提示词、或别的客户端版本发来的旧表单。那种输入
* 由休眠的渲染路径兜底,钉在
* `apps/web/tests/components/question-form-direction-cards-dead-end.test.tsx`。
*
* 本文件按根 `AGENTS.md` 落在 `e2e/tests/` —— 它同时观察 apps/daemon、
* packages/contracts 与 plugins 三处,是跨包一致性检查。纯文件读取,不起 runtime。
*/
import { readFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
const REPO = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
const read = (rel: string): string => readFileSync(path.join(REPO, rel), 'utf-8');
/**
* 七条**会被模型读到**的授权路径。
*
* 前六条和 `question-form-type-parity.test.ts` 的 `PROMPT_PATHS` 逐条对应 ——
* 那边守「类型清单齐不齐」,这边守「设计风格那一项撤干净没有」,两边共用同一份
* 路径认知,少一条都会让这次下线漏出一个口子。
*
* 第七条是 `direction-picker` atom:它**不在** parity 那份清单里,却是这次下线
* 真正的大头 —— 它被 `od-default`(默认设计路由)、`od-next-strategy`、
* `od-new-generation`、`od-tune-collab`、`od-plugin-authoring` 五个官方场景挂在
* `plan` 阶段,整段 SKILL.md 会被 `renderActiveStageBlock` 拼进系统提示词
* (`apps/daemon/tests/prompts/core-slim.test.ts` 那条用例就是在断言这件事)。
* 只改前六条、留着这一条,默认路由照旧会告诉模型「你可以出设计风格卡」。
*/
const PROMPT_PATHS: { rel: string; reachable: string }[] = [
{ rel: 'apps/daemon/src/prompts/core-slim.ts', reachable: '默认设计会话(slim 是默认)' },
{ rel: 'plugins/_official/atoms/discovery-question-form/SKILL.md', reachable: '插件 / OD Next' },
{ rel: 'apps/daemon/src/prompts/system.ts', reachable: 'ask 模式 / 媒体面 / classic' },
{ rel: 'apps/daemon/src/prompts/discovery.ts', reachable: '仅 OD_PROMPT_CORE=classic' },
{ rel: 'packages/contracts/src/prompts/system.ts', reachable: '当前无运行时消费者(镜像)' },
{ rel: 'packages/contracts/src/prompts/discovery.ts', reachable: '当前无运行时消费者(镜像)' },
{
rel: 'plugins/_official/atoms/direction-picker/SKILL.md',
reachable: 'od-default / od-next-strategy / od-new-generation / od-tune-collab / od-plugin-authoring 的 plan 阶段',
},
];
describe('设计风格选择题已从提示词整题下线', () => {
it('七条路径都还在,没有被悄悄挪走', () => {
for (const { rel } of PROMPT_PATHS) {
expect(() => read(rel), `${rel} 不见了 —— 挪动位置要同时更新本测试`).not.toThrow();
}
});
it('没有一条路径再向模型提供 `direction-cards` 这个类型', () => {
for (const { rel, reachable } of PROMPT_PATHS) {
/*
* 连**否定句**也不许留(「不要发 direction-cards」这种)。
* 一句「不要用 X」同时也在告诉模型「有个 X 可以用」—— 产品要的是
* 「不问了」,不是「问之前先想想」。这条对着裁决那句「让 agent 感知到
* question-form 能出设计风格的那些提示词下掉」,感知本身就是要撤的东西。
*/
expect(read(rel), `${rel}(${reachable})还在提 direction-cards`)
.not.toMatch(/direction-cards/);
}
});
it('没有一条路径再教 host 自带的视觉风格目录怎么用', () => {
for (const { rel } of PROMPT_PATHS) {
const src = read(rel);
expect(src, `${rel} 还在教 host 风格目录`).not.toMatch(/visual-style catalog/i);
expect(src, `${rel} 还在教 host 风格目录`).not.toMatch(/visual style catalog/i);
expect(src, `${rel} 还在承诺 host 会给预览图`).not.toMatch(/preview (images|assets)/i);
}
});
it('开场简报的示例表单里不再摆一道视觉风格题', () => {
/*
* `tone` 是第二个入口,而且比 `direction-cards` 更隐蔽:它长得像一道普通单选,
* 渲染时却被 `QuestionForm.tsx` 的 `asksVisualDirection`(`q.id === 'tone'`)
* 认走,换成整份风格目录。示例表单里摆着它,等于每一轮开场都在教模型问这道题。
*/
for (const rel of [
'apps/daemon/src/prompts/discovery.ts',
'packages/contracts/src/prompts/discovery.ts',
]) {
const src = read(rel);
expect(src, `${rel} 的示例简报还带着 tone 那道题`).not.toMatch(/"id":\s*"tone"/);
expect(src, `${rel} 还在别处点名 tone 这道题`).not.toMatch(/`tone`/);
}
});
it('渲染器**仍然**认得 direction-cards —— 休眠件的安全网不许一起拆掉', () => {
/*
* 这一条从一开始就是绿的,它防的是**改过头**:有人顺手把类型从渲染器里也删掉。
* 提示词撤了不等于线上不会再来 —— 缓存的旧提示词、别的客户端版本、模型自己
* 记住的旧格式都可能发来 `direction-cards`。渲染路径必须继续认它,
* 否则那道题会变成一块什么都没有的空白。产品原话也是「后续可能要找回」。
*/
const src = read('apps/web/src/artifacts/question-form.ts');
const union = /export type QuestionType =([\s\S]*?);/.exec(src);
expect(union, 'QuestionType 联合类型抽不出来 —— 抽取逻辑坏了').toBeTruthy();
expect(union![1], '渲染器把 direction-cards 也删了 —— 休眠件失去安全网')
.toMatch(/'direction-cards'/);
});
});
@@ -184,6 +184,17 @@ export const DESIGN_DIRECTIONS: DesignDirection[] = [
];
/**
* ⚠️ **休眠件(T69,2026-09-07)** —— 说明书在
* `apps/web/src/runtime/visual-style-catalog.ts` 文件头。
*
* 这个函数**在本次改动之前就已经没有任何调用点**(全仓搜 `renderDirectionFormBody`
* 只搜得到定义),设计风格选择题从提示词整题下线之后更不会有。留着不删是因为
* 产品明说「后续可能要找回」,而它是那条路上现成的一块。
*
* ⚠️ 同文件里**读答案**那一半(`od tools directions` / `catalogue identity` 那段)
* **是活的,别一起清掉**:旧表单交上来的 `value` / `foundation` / `guidance`
* 仍要读得懂。撤的是**发问**,不是**读答案**。
*
* Render the direction-picker form body for emission as a `<question-form>`.
* Uses the `direction-cards` question type so the UI renders each option
* as a rich card (palette swatches + type sample + mood blurb + refs)
+4 -8
View File
@@ -28,8 +28,7 @@ Three hard rules govern every new design task. They are not optional. The user i
Active design system exception: if a later section in this same system prompt is titled \`## Active design system\`, the user has already selected the brand and visual direction. In that case:
- Treat the active design system's palette, typography, spacing, and component rules as the visual direction.
- Do not ask the user to pick a separate theme color, visual direction, palette, typography mood, or direction card.
- Do not emit a direction question-form or any \`direction-cards\` question for this project.
- Do not ask the user to pick a separate theme color, visual direction, palette, or typography mood.
- In any discovery form, drop brand/direction/theme-color questions unless the user explicitly asks to switch away from the active design system.
- If an older discovery answer says \`brand: "Pick a direction for me"\`, ignore Branch A and proceed to RULE 3 using the active design system.
@@ -53,8 +52,6 @@ When the Active plugin / Active skill is \`od-default\` or "Default design route
"options": ["Slide deck / pitch", "Single web prototype / landing", "Multi-screen app prototype", "Dashboard / tool UI", "Editorial / marketing page"] },
{ "id": "audience", "label": "Who is this for?", "type": "text",
"placeholder": "e.g. early-stage investors, dev-tools buyers, internal exec review" },
{ "id": "tone", "label": "Visual tone", "type": "radio",
"options": ["Editorial / magazine", "Modern minimal", "Playful / illustrative", "Tech / utility", "Luxury / refined", "Brutalist / experimental", "Human / approachable"] },
{ "id": "brand", "label": "Brand context", "type": "radio", "default": "pick_direction",
"options": [
{ "label": "Pick a direction for me", "value": "pick_direction" },
@@ -70,8 +67,7 @@ When the Active plugin / Active skill is \`od-default\` or "Default design route
Form authoring rules:
- Body must be valid JSON. No comments. No trailing commas.
- \`type\` is one of: \`radio\`, \`checkbox\`, \`select\`, \`text\`, \`textarea\`, \`number\`, \`range\`, \`date\`, \`time\`, \`datetime-local\`, \`color\`, \`url\`, \`email\`, \`tel\`, \`file\`, \`switch\`, \`direction-cards\`.
- \`direction-cards\` is a trigger for Open Design's host-owned visual-style catalog. Emit only the question's stable \`id\`, localized \`label\`, \`type: "direction-cards"\`, and \`required\` when appropriate; omit \`options\`, \`cards\`, \`variant\`, and \`defaultValue\`. The host selects the versioned catalog, preview images, recommendation, and stable style ids from the project kind. Do not spend output tokens inventing card metadata or preview assets.
- \`type\` is one of: \`radio\`, \`checkbox\`, \`select\`, \`text\`, \`textarea\`, \`number\`, \`range\`, \`date\`, \`time\`, \`datetime-local\`, \`color\`, \`url\`, \`email\`, \`tel\`, \`file\`, \`switch\`.
- Use the most expressive mainstream web form control for the information you need: sliders for numeric intensity, color for brand/accent picks, date/time for deadlines, url/email/tel for contact/reference fields, file for upload requests, switch for binary preferences, and textarea only for genuinely open prose.
- At most 6-7 options per question; merge near-duplicates instead of listing more.
- Choose \`radio\` vs \`select\` by option count, not importance: \`radio\` for a short list, \`select\` once it runs long (languages, timezones, voices). \`checkbox\` is always a plain list.
@@ -81,8 +77,8 @@ Form authoring rules:
- When the selected or likely output is a slide deck / pitch deck, include a \`speakerNotes\` switch with \`defaultValue: true\` unless project metadata or plugin inputs already supply \`speakerNotes\`.
- For reference images, brand specs, PDFs, slide/docs, screenshots, source exports, or any brief that asks the user to "upload/paste a file", include a \`type: "file"\` question in the same form instead of asking in prose after the form. Use \`multiple: true\` when several assets are useful, and \`accept\` such as \`"image/*"\`, \`".pdf,.doc,.docx"\`, or a comma-separated mix when the needed source type is known. Selected files are uploaded into Design Files and submitted as attached/context files on the answer turn.
- For \`checkbox\` questions, include \`maxSelections\` when the user should choose only a limited number of options. Do not encode limits only in the label text.
- The host automatically renders a localized "Other" escape hatch (a chip that expands into a type-in field) on every finite-choice question EXCEPT the visual ones (\`radio\`, \`checkbox\`, \`select\`) — do NOT author your own catch-all "Other …" / "I'll describe" option there; it would duplicate the host's. A \`direction-cards\` question, and a \`tone\` question in a project that has a visual style, render as the built-in style catalog, which has no type-in field: the delivered design draws none, so the host cannot promise one. State an out in the question text if the user needs one. Leave \`allowCustom\` unset or \`true\`; add localized \`customLabel\` / \`customPlaceholder\` when the default copy is not specific enough. Only set \`allowCustom: false\` when the downstream system truly requires one exact machine id.
- Prefill every non-visual question with a recommended \`default\` inferred from the brief, project metadata, and plugin inputs — an option \`value\` for \`radio\`/\`select\`, an array of option \`value\`s for \`checkbox\`, or concrete suggested text for free-text fields, never placeholder filler. The goal is a form the user can submit unchanged and still get a sensible build; omit \`default\` only when no reasonable recommendation exists (e.g. a \`file\` upload). A \`direction-cards\` question is the exception: the host-owned catalog owns its recommendation, so do not invent a \`defaultValue\`. Place the \`default\` key before \`options\` in each other question object, as the example forms above do — the host renders forms token-by-token, and a \`default\` that trails a long \`options\` array reaches the user late.
- The host automatically renders a localized "Other" escape hatch (a chip that expands into a type-in field) on every finite-choice question EXCEPT the visual ones (\`radio\`, \`checkbox\`, \`select\`) — do NOT author your own catch-all "Other …" / "I'll describe" option there; it would duplicate the host's. Leave \`allowCustom\` unset or \`true\`; add localized \`customLabel\` / \`customPlaceholder\` when the default copy is not specific enough. Only set \`allowCustom: false\` when the downstream system truly requires one exact machine id.
- Prefill every non-visual question with a recommended \`default\` inferred from the brief, project metadata, and plugin inputs — an option \`value\` for \`radio\`/\`select\`, an array of option \`value\`s for \`checkbox\`, or concrete suggested text for free-text fields, never placeholder filler. The goal is a form the user can submit unchanged and still get a sensible build; omit \`default\` only when no reasonable recommendation exists (e.g. a \`file\` upload). Place the \`default\` key before \`options\` in each other question object, as the example forms above do — the host renders forms token-by-token, and a \`default\` that trails a long \`options\` array reaches the user late.
- Localize every user-facing string in the form (\`title\`, the per-question \`label\`, \`placeholder\`, and option \`label\`s) to the user's chat language — write what a native speaker would naturally say, never a word-for-word translation (the Chinese title is 快速确认 · 30秒, not the literal 快速简报). Set the top-level \`"lang"\` field to the BCP-47 tag of that language (e.g. \`"zh-CN"\`, \`"ja"\`) so the host renders its built-in controls (the "Other" chip, the custom-answer field) in the same language. \`id\`, \`type\`, option \`value\`, and the stable branch values (\`pick_direction\`, \`brand_spec\`, \`reference_match\`) MUST stay in English because later branch rules match against them.
- If you keep the \`brand\` question, its \`id\` must stay \`"brand"\`. Its three default branch values must stay exactly \`"pick_direction"\`, \`"brand_spec"\`, and \`"reference_match"\` even if you localize the labels.
- If the initial brief already includes a brand spec, brand-guide attachment, reference URL, or screenshot, you may drop the \`brand\` question as already answered, but you must still treat that provided source as Branch A below.
+2 -2
View File
@@ -177,7 +177,7 @@ const ACTIVE_DESIGN_SYSTEM_VISUAL_DIRECTION_OVERRIDE = `
Active design system exception: the active design system is the visual direction for this project. Use its DESIGN.md palette, typography, spacing, component rules, and theme tokens as the source of truth for color and mood.
- Do not ask the user to pick a separate theme color, visual direction, palette, typography mood, or direction card.
- Do not emit a direction question-form, a \`direction-cards\` picker, or any visual-direction card while an active design system is present.
- Do not emit a direction question-form while an active design system is present.
- If an earlier discovery answer asks to "Pick a direction for me", treat that as already satisfied by the active design system and continue with the plan.
- When a downstream framework mentions "active direction" or "theme tokens", bind those fields from the active design system instead of the built-in direction library.
`;
@@ -445,7 +445,7 @@ export function composeSystemPrompt({
// and a BYOK/API chat route follow-up choices through the same surface
// instead of drifting back to plain markdown option lists.
parts.push(
"\n\n---\n\n## Structured clarification on any turn\n\nWhen clarification is materially necessary and the answer benefits from structured input, emit a `<question-form>` block instead of writing a bulleted list of options in markdown. The host renders it inline in the originating assistant message; a markdown list renders as plain text and forces the user to type a reply. Use the richest appropriate web form controls (`radio`, `checkbox`, `select`, `text`, `textarea`, `number`, `range`, `date`, `time`, `datetime-local`, `color`, `url`, `email`, `tel`, `file`, `switch`, or `direction-cards`). For a `direction-cards` question, emit only its intent fields; omit `options`, `cards`, `variant`, and `defaultValue` because the OpenDesign host supplies the project-kind visual catalog, previews, recommendation, and stable style ids. When the clarification needs reference images, source docs, screenshots, or other user files, combine a `type: \"file\"` question with the text/options in the same form; selected files are uploaded into Design Files and submitted as attached/context files on the answer turn. For every finite-choice question, keep user control by leaving `allowCustom` unset or setting it to `true`, and add localized `customLabel` / `customPlaceholder` when useful. Use free-form prose questions only when a form would add no structure. Do NOT also duplicate the form's questions as markdown text alongside it.\n\n`<question-form>` is assistant text for the OpenDesign UI, not a native tool call. If you need to clarify direction, emit the complete `<question-form>...</question-form>` block directly in the assistant message before any TodoWrite, file write/edit, Bash, or other native tool call. Do not stop after an introductory sentence such as \"先确认一下方向:\"; the same message must include the full form.\n\nAt most 6-7 options per question; merge near-duplicates instead of listing more. Choose `radio` vs `select` by option count, not importance: `radio` for a short list, `select` once it runs long (languages, timezones, voices); `checkbox` is always a plain list. `select` options may carry `group` (first group expands, the rest collapse) and `trailingLabel` (a short end-of-row code such as `ZH-CN`); both optional. Label options in the user's words, not jargon: \"Magazine-style layout\", not \"Editorial\". Reword only `label`; never change a stable `value`. Keep each `label` under ~40 characters; put anything longer in `description`.",
"\n\n---\n\n## Structured clarification on any turn\n\nWhen clarification is materially necessary and the answer benefits from structured input, emit a `<question-form>` block instead of writing a bulleted list of options in markdown. The host renders it inline in the originating assistant message; a markdown list renders as plain text and forces the user to type a reply. Use the richest appropriate web form controls (`radio`, `checkbox`, `select`, `text`, `textarea`, `number`, `range`, `date`, `time`, `datetime-local`, `color`, `url`, `email`, `tel`, `file`, `switch`). When the clarification needs reference images, source docs, screenshots, or other user files, combine a `type: \"file\"` question with the text/options in the same form; selected files are uploaded into Design Files and submitted as attached/context files on the answer turn. For every finite-choice question, keep user control by leaving `allowCustom` unset or setting it to `true`, and add localized `customLabel` / `customPlaceholder` when useful. Use free-form prose questions only when a form would add no structure. Do NOT also duplicate the form's questions as markdown text alongside it.\n\n`<question-form>` is assistant text for the OpenDesign UI, not a native tool call. If you need to clarify direction, emit the complete `<question-form>...</question-form>` block directly in the assistant message before any TodoWrite, file write/edit, Bash, or other native tool call. Do not stop after an introductory sentence such as \"先确认一下方向:\"; the same message must include the full form.\n\nAt most 6-7 options per question; merge near-duplicates instead of listing more. Choose `radio` vs `select` by option count, not importance: `radio` for a short list, `select` once it runs long (languages, timezones, voices); `checkbox` is always a plain list. `select` options may carry `group` (first group expands, the rest collapse) and `trailingLabel` (a short end-of-row code such as `ZH-CN`); both optional. Label options in the user's words, not jargon: \"Magazine-style layout\", not \"Editorial\". Reword only `label`; never change a stable `value`. Keep each `label` under ~40 characters; put anything longer in `description`.",
/* 与 daemon 那份 `prompts/system.ts` 的「How your turn is rendered」逐字对应 ——
两边措辞必须一致,否则 API/BYOK 模式和 daemon 模式对同一件事给模型两种说法。 */
"\n\n---\n\n## How your turn is rendered\n\n" +
+15 -8
View File
@@ -45,9 +45,8 @@ describe('DISCOVERY_AND_PHILOSOPHY (contracts copy) — TodoWrite plan item coun
// it is what makes Ask cheaper than Design/Plan.
expect(prompt).not.toContain(DISCOVERY_AND_PHILOSOPHY);
expect(prompt).not.toContain('# Identity and workflow charter (background)');
expect(prompt).toContain(
'For a `direction-cards` question, emit only its intent fields; omit `options`, `cards`, `variant`, and `defaultValue`',
);
// T69(2026-09-07):设计风格选择题整题下线,Ask 模式也不再提它
expect(prompt).not.toContain('direction-cards');
});
it('uses a top-level Plan mode override that suppresses artifact discovery forms', () => {
@@ -95,12 +94,20 @@ describe('DISCOVERY_AND_PHILOSOPHY (contracts copy) — prompt routing parity',
expect(DISCOVERY_AND_PHILOSOPHY).not.toContain('<question-form id="task-type"');
});
it('keeps the host-owned direction-cards contract in API/BYOK prompts', () => {
/**
* T69(2026-09-07):设计风格选择题从提示词整题下线,产品逐字「**不问了**」。
* 原用例守的是「API/BYOK 这条路也要教 host 目录契约」,现在守它不再教。
*
* ⚠️ **答案解读那一半故意留着**(`od tools directions` 那条):缓存的旧提示词、
* 旧客户端、模型记住的旧格式都还可能把一份 Host 目录答案交上来,那时 agent
* 必须仍然知道 `value` / `foundation` / `guidance` 怎么用 —— 这和渲染器继续
* 认得 `direction-cards` 是同一件事的两面(见 e2e `DORMANT_TYPES`)。
* 撤的是**发问的能力**,不是**读答案的能力**。
*/
it('API/BYOK 提示词不再教怎么出设计风格题,但仍会读旧答案', () => {
const prompt = composeSystemPrompt({ metadata: { kind: 'other' } as any });
expect(prompt).toContain(
"`direction-cards` is a trigger for Open Design's host-owned visual-style catalog",
);
expect(prompt).toContain('omit `options`, `cards`, `variant`, and `defaultValue`');
expect(prompt).not.toContain('direction-cards');
expect(prompt).not.toContain("host-owned visual-style catalog");
expect(prompt).toContain(
'the Host value is catalogue identity and must not be passed to `od tools directions`',
);
@@ -1,6 +1,6 @@
---
name: direction-picker
description: Optional host-owned visual catalog for users who explicitly ask to compare directions.
description: Resolves the visual direction at the plan stage from the brief and design system, without asking the user.
od:
scenario: general
mode: planning
@@ -8,37 +8,37 @@ od:
# Direction picker
Generative work benefits from explicit divergence before it converges. This
atom lets the user compare Open Design's versioned visual-style catalog when
they explicitly ask to see or compare direction options. Only in that case,
emit one inline `<question-form>` with one `direction-cards` question. The
submitted choice returns as the next user message.
Converging work needs one committed visual direction before the build starts.
This atom owns that moment in the `plan` stage: decide the direction, state it
in one line, and lock onto it.
`direction-cards` is a Host-owned catalog trigger, not an invitation for the
agent to draft cards. Emit only the question's stable `id`, localized `label`,
`type: "direction-cards"`, and `required` when appropriate. Omit `options`,
`cards`, `variant`, and `defaultValue`: the Host selects the catalog, preview
images, recommendation, and stable style ids from the project kind.
Resolve the direction from what you already have, in this order:
The submitted answer contains three parts: the stable Host catalogue `value`,
a resolvable direction-library `foundation`, and the selected card's visual
`guidance`. Use the foundation for deterministic palette/font tokens and apply
the guidance as its refinement. Never pass the Host value to
`od tools directions`.
1. An active design system — its DESIGN.md palette, typography, spacing, and
component rules **are** the direction. Bind its tokens and stop here.
2. A brand spec, reference URL, or screenshot the user supplied — parse that
source directly.
3. Otherwise, infer the best-matching direction yourself from the brief's
domain, audience, and tone, then bind its visual tokens. If the runtime
provides only an index of direction ids and names, run
`"$OD_NODE_BIN" "$OD_BIN" tools directions --id <id>` to retrieve the full
specification — never infer colors or fonts from the name alone.
The presence of this atom or the `plan` stage does not trigger a picker. Do not
emit direction cards proactively. When the user has not explicitly requested
options, infer a fitting direction from the brief, active design system, and
known context, then continue.
**Do not ask the user to choose a visual direction.** Not as a question-form,
not as a markdown list of options, not as a "which of these feels right?"
follow-up. The direction is yours to resolve; asking spends the user's turn on
a decision they hired the agent to make.
## Convergence
When a picker was explicitly requested, the atom completes when the submitted
form answer contains a direction id. The agent's next turn must lock onto that
direction — backtracking forces a fresh devloop iteration of the picker stage.
The atom completes when the plan states the chosen direction. The agent's next
turn must build against that direction — backtracking forces a fresh devloop
iteration of the plan stage.
## Anti-patterns the prompt fragment forbids
- Agent-authored direction options, card metadata, preview assets, or variants.
- Asking the user to pick, compare, or confirm a visual direction.
- Locking the user into a single direction with cosmetic alternates
(every direction must be a defensible standalone bet).
(a stated direction must be a defensible standalone bet).
- Inferring palette or typography from a direction's name instead of
resolving its specification.
@@ -5,7 +5,7 @@
"title": "Direction picker",
"version": "0.1.0",
"publishedAt": "2026-05-09T13:00:27Z",
"description": "Optional host-owned visual catalog for users who explicitly ask to compare directions.",
"description": "Resolves the visual direction at the plan stage from the brief and design system, without asking the user.",
"license": "MIT",
"author": {
"name": "OpenDesign",
@@ -62,17 +62,14 @@ Each entry in the top-level `questions` array uses:
- `label`: user-facing question copy.
- `type`: one of `radio`, `checkbox`, `select`, `text`, `textarea`,
`number`, `range`, `date`, `time`, `datetime-local`, `color`, `url`,
`email`, `tel`, `file`, `switch`, or `direction-cards`.
- `options`: required for choice controls except `direction-cards`; strings are
`email`, `tel`, `file`, or `switch`.
- `options`: required for choice controls; strings are
allowed, or objects with localized `label` and stable `value`.
- At most 6-7 options per question; merge near-duplicates instead of listing more.
- Choose `radio` vs `select` by option count, not importance: `radio` for a short list, `select` once it runs long (languages, timezones, voices). `checkbox` is always a plain list.
- `select` options may carry `group` (first group expands, the rest collapse) and `trailingLabel` (a short end-of-row code such as `ZH-CN`). Both optional.
- Label options in the user's words, not jargon: "Magazine-style layout", not "Editorial". Reword only `label`; never change a stable `value`.
- Keep each `label` under ~40 characters; put anything longer in `description`.
- `direction-cards`: a Host-owned visual-style catalog trigger. Emit only the
question's `id`, localized `label`, `type`, and `required` when appropriate;
omit `options`, `cards`, `variant`, and `defaultValue`.
- `allowCustom`: leave unset or set to `true` for finite-choice controls so
users can type their own answer instead of accepting only generated options.
Set `allowCustom: false` only when the downstream system needs an exact
@@ -3582,7 +3582,7 @@
"capabilitiesSummary": [
"prompt:inject"
],
"description": "Optional 3-5 direction picker for users who explicitly ask to compare visual directions.",
"description": "Resolves the visual direction at the plan stage from the brief and design system, without asking the user.",
"tags": [
"atom",
"first-party",