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); + }); + }); });