From 4aea5fdb01b6a33fd5db5bc9310509e6056e9cdf Mon Sep 17 00:00:00 2001 From: elihahah666 Date: Wed, 3 Jun 2026 20:56:44 +0800 Subject: [PATCH] feat: skip discovery questions for unmodified example prompts (#3257) * feat: skip discovery questions for unmodified example prompts When a user selects a curated example prompt from the gallery and sends it without modification, the agent now skips all discovery/clarification questions and directly generates the artifact at full quality. Adds an `examplePrompt` metadata flag that injects a dedicated system prompt override instructing the agent to treat the prompt as a complete creative brief requiring showcase-quality generation. Co-Authored-By: Claude Opus 4 * feat: enrich example prompt override with title and pre-filled brief - Pass example prompt title and artifact-type-specific discovery brief through metadata so the agent receives full context without asking - Only skip discovery on the user's first example prompt usage (localStorage gated), subsequent uses follow normal discovery flow - Convert EXAMPLE_PROMPT_OVERRIDE from static string to dynamic builder that interpolates title and brief key-value pairs * fix: prevent handlePromptChange from clearing examplePromptInfoRef The ref is now only cleared by onExamplePromptStatusChange(null) which HomeHero triggers when the user actually edits the text or clicks clear. This prevents the race where usePromptExample sets the ref and then onPromptChange immediately clears it in the same tick. * fix: honor example-prompt discovery skip in API/BYOK mode API/BYOK runs compose their system prompt through @open-design/contracts, whose ProjectMetadata and composeSystemPrompt only knew about skipDiscoveryBrief. Unmodified gallery example prompts therefore skipped discovery in daemon-backed runs but still asked discovery questions in API mode, making the feature mode-dependent. Mirror the daemon's examplePrompt/examplePromptTitle/examplePromptBrief metadata and buildExamplePromptOverride into contracts so both composers behave identically. Generated-By: looper 0.8.1 (runner=fixer, agent=claude-code) --------- Co-authored-by: qiongyu1999 <2694684348@qq.com> Co-authored-by: Claude Opus 4 --- apps/daemon/src/prompts/system.ts | 37 +++++++++++++- apps/web/src/components/EntryShell.tsx | 5 ++ apps/web/src/components/HomeHero.tsx | 51 +++++++++++++++++++ apps/web/src/components/HomeView.tsx | 16 +++++- apps/web/src/components/PluginLoopHome.tsx | 1 + packages/contracts/src/api/projects.ts | 8 +++ packages/contracts/src/prompts/system.ts | 34 ++++++++++++- .../tests/system-prompt-api-mode.test.ts | 48 +++++++++++++++++ 8 files changed, 196 insertions(+), 4 deletions(-) diff --git a/apps/daemon/src/prompts/system.ts b/apps/daemon/src/prompts/system.ts index d2b922f670..8f3802d24e 100644 --- a/apps/daemon/src/prompts/system.ts +++ b/apps/daemon/src/prompts/system.ts @@ -129,6 +129,9 @@ type ProjectMetadata = { platformTargets?: string[] | null; inspirationDesignSystemIds?: string[]; skipDiscoveryBrief?: boolean | null; + examplePrompt?: boolean | null; + examplePromptTitle?: string | null; + examplePromptBrief?: Record | null; imageModel?: string | null; imageAspect?: string | null; imageStyle?: string | null; @@ -278,6 +281,35 @@ printf '%s\\n' "\$last" For the best fal image model use \`--model flux-pro-ultra\`. For video use \`--model veo-3-fal\` or \`--model wan-2.1-t2v\`. Always pass \`--surface\` explicitly (\`image\`, \`video\`, or \`audio\`). Any \`fal-ai/*\` path (e.g. \`fal-ai/flux/schnell\`, \`fal-ai/wan-i2v\`) is also a valid \`--model\` value for image/video — pass it through as-is without substitution.`; +export function buildExamplePromptOverride( + title?: string | null, + brief?: Record | null, +): string { + let text = `# Example prompt mode — full-quality direct generation + +The user selected a curated example prompt from the gallery and sent it without modification. This prompt is a complete, self-contained creative brief that has been carefully designed to produce a showcase-quality artifact.`; + + if (title) { + text += `\n\nSelected example: "${title}"`; + } + + if (brief && Object.keys(brief).length > 0) { + text += `\n\nPre-filled creative brief (treat as if the user already answered all discovery questions):`; + for (const [key, value] of Object.entries(brief)) { + text += `\n- ${key.replace(/_/g, ' ')}: ${value}`; + } + } + + text += `\n\nRules: +1. Do NOT emit \`\`, do NOT show "Quick brief — 30 seconds", and do NOT ask any clarifying questions. +2. Treat the user's message as the FULL specification — it contains all visual direction, content themes, and structural intent needed. +3. Generate the artifact at your absolute highest quality. This is a showcase piece — match or exceed the standard of a hand-crafted design. +4. Infer any unspecified details (copy, layout choices, imagery descriptions) in a way that is maximally coherent with the stated creative direction. +5. Proceed directly to planning and building. Output your TodoWrite plan and then the artifact immediately.`; + + return text; +} + const ACTIVE_DESIGN_SYSTEM_VISUAL_DIRECTION_OVERRIDE = ` --- @@ -510,7 +542,10 @@ export function composeSystemPrompt({ metadata?.kind === 'video' || metadata?.kind === 'audio'; - if (metadata?.skipDiscoveryBrief === true) { + if (metadata?.examplePrompt === true) { + parts.push(buildExamplePromptOverride(metadata.examplePromptTitle, metadata.examplePromptBrief)); + parts.push('\n\n---\n\n'); + } else if (metadata?.skipDiscoveryBrief === true) { parts.push(SKIP_DISCOVERY_BRIEF_OVERRIDE); parts.push('\n\n---\n\n'); } diff --git a/apps/web/src/components/EntryShell.tsx b/apps/web/src/components/EntryShell.tsx index 94b6e12150..a09bcf7a9f 100644 --- a/apps/web/src/components/EntryShell.tsx +++ b/apps/web/src/components/EntryShell.tsx @@ -526,6 +526,11 @@ export function EntryShell({ ? { contextConnectors: payload.contextConnectors } : {}), ...(payload.workingDir ? { userWorkingDir: payload.workingDir } : {}), + ...(payload.examplePromptContext ? { + examplePrompt: true, + examplePromptTitle: payload.examplePromptContext.title, + examplePromptBrief: payload.examplePromptContext.brief, + } : {}), }; onCreateProject({ name, diff --git a/apps/web/src/components/HomeHero.tsx b/apps/web/src/components/HomeHero.tsx index da7edbe121..af0142c05e 100644 --- a/apps/web/src/components/HomeHero.tsx +++ b/apps/web/src/components/HomeHero.tsx @@ -61,6 +61,12 @@ export interface HomeHeroSubmitHandler { (): void; } +export interface ExamplePromptInfo { + title: string; + artifactType: string; + brief: Record; +} + interface Props { prompt: string; onPromptChange: (value: string) => void; @@ -107,6 +113,7 @@ interface Props { contextItemCount: number; error: string | null; showActivePluginChip?: boolean; + onExamplePromptStatusChange?: (info: ExamplePromptInfo | null) => void; } interface HomeHeroDesignSystemOption { @@ -192,6 +199,7 @@ export const HomeHero = forwardRef(function HomeHero contextItemCount, error, showActivePluginChip = true, + onExamplePromptStatusChange, }, ref, ) { @@ -541,6 +549,7 @@ export const HomeHero = forwardRef(function HomeHero function clearSelectedPromptExample() { if (selectedPromptExample) { onPromptChange(''); + onExamplePromptStatusChange?.(null); } setSelectedPromptExample(null); } @@ -550,6 +559,11 @@ export const HomeHero = forwardRef(function HomeHero label: promptExampleChipLabel(example), promptText: example, }); + onExamplePromptStatusChange?.({ + title: promptExampleChipLabel(example), + artifactType: activeChipId ?? 'prototype', + brief: briefForChipId(activeChipId ?? 'prototype'), + }); onPromptChange(example); setSelectedIndex(0); requestAnimationFrame(() => { @@ -567,6 +581,11 @@ export const HomeHero = forwardRef(function HomeHero label: record.title, promptText, }); + onExamplePromptStatusChange?.({ + title: record.title, + artifactType: chipId, + brief: briefForPluginPreset(record, chipId), + }); onPickExamplePlugin(record, chipId, promptText); } @@ -826,6 +845,7 @@ export const HomeHero = forwardRef(function HomeHero onPromptChange(e.target.value); if (selectedPromptExample && e.target.value !== selectedPromptExample.promptText) { setSelectedPromptExample(null); + onExamplePromptStatusChange?.(null); } setSelectedIndex(0); }} @@ -3070,3 +3090,34 @@ function homeHeroChipPromptExamples(chipId: string, locale: Locale): string[] { function isChineseLocale(locale: Locale): boolean { return locale === 'zh-CN' || locale === 'zh-TW'; } + +function briefForChipId(chipId: string): Record { + switch (chipId) { + case 'prototype': + return { artifact_type: 'web prototype', audience: 'product evaluators', fidelity: 'high-fidelity' }; + case 'deck': + return { artifact_type: 'pitch deck / presentation', audience: 'decision makers', slide_count: '10-15 pages' }; + case 'image': + return { artifact_type: 'image', style: 'cinematic, high-quality, on-brand' }; + case 'video': + return { artifact_type: 'video', style: 'cinematic, high-quality, on-brand' }; + case 'hyperframes': + return { artifact_type: 'motion graphic / animated sequence', style: 'cinematic, polished transitions' }; + case 'audio': + return { artifact_type: 'audio', style: 'professional, polished, brand-appropriate' }; + default: + return { artifact_type: chipId }; + } +} + +function briefForPluginPreset(record: InstalledPluginRecord, chipId: string): Record { + const brief: Record = { ...briefForChipId(chipId) }; + const fields = record.manifest?.od?.inputs ?? []; + for (const field of fields) { + const value = field.default ?? field.placeholder; + if (value != null && typeof value === 'string' && value.trim()) { + brief[field.name] = value; + } + } + return brief; +} diff --git a/apps/web/src/components/HomeView.tsx b/apps/web/src/components/HomeView.tsx index 35409c74f9..71a4b95bcc 100644 --- a/apps/web/src/components/HomeView.tsx +++ b/apps/web/src/components/HomeView.tsx @@ -7,7 +7,7 @@ // surface by lifting its plugin orchestration up here so the prompt // textarea can live centered in the hero. -import { useEffect, useMemo, useRef, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import type { ApplyResult, ConnectorDetail, @@ -51,7 +51,7 @@ import type { SkillSummary, } from '../types'; import { inlineMentionToken } from '../utils/inlineMentions'; -import { HomeHero } from './HomeHero'; +import { HomeHero, type ExamplePromptInfo } from './HomeHero'; import { findChip, HOME_HERO_CHIPS, type HomeHeroChip } from './home-hero/chips'; import { buildHomeMediaComposer, @@ -240,6 +240,10 @@ export function HomeView({ const [mcpLoading, setMcpLoading] = useState(true); const [prompt, setPrompt] = useState(''); const [promptEditedByUser, setPromptEditedByUser] = useState(false); + const examplePromptInfoRef = useRef(null); + const handleExamplePromptStatusChange = useCallback((info: ExamplePromptInfo | null) => { + examplePromptInfoRef.current = info; + }, []); const [error, setError] = useState(null); const [designSystemLogoById, setDesignSystemLogoById] = useState>({}); const [elevenLabsVoices, setElevenLabsVoices] = useState([]); @@ -1271,6 +1275,13 @@ export function HomeView({ contextMcpServers, contextConnectors, attachments: stagedFiles, + ...(() => { + if (!examplePromptInfoRef.current) return {}; + const key = 'od:example-prompt-used'; + if (localStorage.getItem(key)) return {}; + localStorage.setItem(key, '1'); + return { examplePromptContext: examplePromptInfoRef.current }; + })(), }); } @@ -1330,6 +1341,7 @@ export function HomeView({ onPickChip={pickChip} contextItemCount={contextItemCount} error={error} + onExamplePromptStatusChange={handleExamplePromptStatusChange} /> }; } interface Props { diff --git a/packages/contracts/src/api/projects.ts b/packages/contracts/src/api/projects.ts index ce9e818831..6037ecf3b4 100644 --- a/packages/contracts/src/api/projects.ts +++ b/packages/contracts/src/api/projects.ts @@ -153,6 +153,14 @@ export interface ProjectMetadata { // Batch/API-created projects can opt out of the initial discovery form so // the first agent turn builds immediately from the submitted brief. skipDiscoveryBrief?: boolean; + // Set when the user submits an unmodified curated example prompt from the + // gallery. Skips discovery AND requests full-quality direct generation, + // treating the curated title/brief as the complete creative brief. Honored + // by both the daemon and contracts (API/BYOK) system-prompt composers so the + // feature is not mode-dependent. + examplePrompt?: boolean; + examplePromptTitle?: string; + examplePromptBrief?: Record; // Plugins selected through @ mentions on Home. These are additive // context references; the explicit "Use plugin" snapshot, when present, // remains the primary executable plugin for the run. diff --git a/packages/contracts/src/prompts/system.ts b/packages/contracts/src/prompts/system.ts index ed20e34235..cacb00cd27 100644 --- a/packages/contracts/src/prompts/system.ts +++ b/packages/contracts/src/prompts/system.ts @@ -124,6 +124,35 @@ export const SKIP_DISCOVERY_BRIEF_OVERRIDE = `# Automated project mode — skip This project was created through the daemon API with \`skipDiscoveryBrief: true\`. Override the discovery rules below: do NOT emit \`\`, do NOT show "Quick brief — 30 seconds", and do NOT ask a first-turn clarification form. Do not call AskUserQuestion, do not emit any question form or choice card, and do not wait for user input. Treat the user's first message and project metadata as the brief, choose reasonable defaults for any missing details, then proceed directly to planning/building under the normal artifact workflow.`; +export function buildExamplePromptOverride( + title?: string | null, + brief?: Record | null, +): string { + let text = `# Example prompt mode — full-quality direct generation + +The user selected a curated example prompt from the gallery and sent it without modification. This prompt is a complete, self-contained creative brief that has been carefully designed to produce a showcase-quality artifact.`; + + if (title) { + text += `\n\nSelected example: "${title}"`; + } + + if (brief && Object.keys(brief).length > 0) { + text += `\n\nPre-filled creative brief (treat as if the user already answered all discovery questions):`; + for (const [key, value] of Object.entries(brief)) { + text += `\n- ${key.replace(/_/g, ' ')}: ${value}`; + } + } + + text += `\n\nRules: +1. Do NOT emit \`\`, do NOT show "Quick brief — 30 seconds", and do NOT ask any clarifying questions. +2. Treat the user's message as the FULL specification — it contains all visual direction, content themes, and structural intent needed. +3. Generate the artifact at your absolute highest quality. This is a showcase piece — match or exceed the standard of a hand-crafted design. +4. Infer any unspecified details (copy, layout choices, imagery descriptions) in a way that is maximally coherent with the stated creative direction. +5. Proceed directly to planning and building. Output your TodoWrite plan and then the artifact immediately.`; + + return text; +} + const ACTIVE_DESIGN_SYSTEM_VISUAL_DIRECTION_OVERRIDE = ` --- @@ -247,7 +276,10 @@ export function composeSystemPrompt({ parts.push('\n\n---\n\n'); } - if (metadata?.skipDiscoveryBrief === true) { + if (metadata?.examplePrompt === true) { + parts.push(buildExamplePromptOverride(metadata.examplePromptTitle, metadata.examplePromptBrief)); + parts.push('\n\n---\n\n'); + } else if (metadata?.skipDiscoveryBrief === true) { parts.push(SKIP_DISCOVERY_BRIEF_OVERRIDE); parts.push('\n\n---\n\n'); } diff --git a/packages/contracts/tests/system-prompt-api-mode.test.ts b/packages/contracts/tests/system-prompt-api-mode.test.ts index bdb2ec8e11..55c8ccdaf1 100644 --- a/packages/contracts/tests/system-prompt-api-mode.test.ts +++ b/packages/contracts/tests/system-prompt-api-mode.test.ts @@ -134,4 +134,52 @@ describe('composeSystemPrompt — API mode (#313)', () => { expect(prompt).toContain('choose reasonable defaults for any missing details'); }); }); + + // Regression coverage for #3257 — example-prompt discovery skip must be + // honored in API/BYOK mode (which composes prompts through this contracts + // composer), not only in daemon-backed runs. Without the examplePrompt + // handling here, the same unmodified gallery prompt skipped discovery in + // daemon mode but still asked discovery questions in API mode. + describe('example prompt mode (#3257)', () => { + it('injects the example-prompt override and skips discovery when metadata.examplePrompt is true', () => { + const prompt = composeSystemPrompt({ + metadata: { kind: 'prototype', examplePrompt: true }, + }); + expect(prompt).toContain('Example prompt mode — full-quality direct generation'); + expect(prompt).toMatch(/do NOT emit `?`?/i); + }); + + it('interpolates the curated title and pre-filled brief', () => { + const prompt = composeSystemPrompt({ + metadata: { + kind: 'prototype', + examplePrompt: true, + examplePromptTitle: 'Neon dashboard', + examplePromptBrief: { target_audience: 'developers', fidelity: 'high' }, + }, + }); + expect(prompt).toContain('Selected example: "Neon dashboard"'); + expect(prompt).toContain('target audience: developers'); + expect(prompt).toContain('fidelity: high'); + }); + + it('pins the example-prompt override above the discovery layer in API mode', () => { + const prompt = composeSystemPrompt({ + streamFormat: 'plain', + metadata: { kind: 'prototype', examplePrompt: true }, + }); + const overrideIdx = prompt.indexOf('Example prompt mode — full-quality direct generation'); + const discoveryIdx = prompt.indexOf('# OD core directives'); + expect(overrideIdx).toBeGreaterThanOrEqual(0); + expect(overrideIdx).toBeLessThan(discoveryIdx); + }); + + it('prefers the example-prompt override over the plain skip-discovery override', () => { + const prompt = composeSystemPrompt({ + metadata: { kind: 'prototype', examplePrompt: true, skipDiscoveryBrief: true }, + }); + expect(prompt).toContain('Example prompt mode — full-quality direct generation'); + expect(prompt).not.toContain(SKIP_DISCOVERY_BRIEF_OVERRIDE); + }); + }); });