diff --git a/apps/daemon/tests/plugins-bundled-scenarios-roster.test.ts b/apps/daemon/tests/plugins-bundled-scenarios-roster.test.ts index 9d70976c87..3706e92e69 100644 --- a/apps/daemon/tests/plugins-bundled-scenarios-roster.test.ts +++ b/apps/daemon/tests/plugins-bundled-scenarios-roster.test.ts @@ -35,6 +35,7 @@ const SIBLINGS = new Map([ ['od-default', { taskKind: 'new-generation' }], ['od-media-generation', { taskKind: 'new-generation' }], ['od-plugin-authoring', { taskKind: 'new-generation' }], + ['od-share-to-community', { taskKind: 'new-generation' }], ['od-design-refine', { taskKind: 'tune-collab' }], ['od-react-export', { taskKind: 'tune-collab' }], ['od-nextjs-export', { taskKind: 'tune-collab' }], diff --git a/apps/web/src/components/AssistantMessage.tsx b/apps/web/src/components/AssistantMessage.tsx index d4879d0c1f..bd309ca4b2 100644 --- a/apps/web/src/components/AssistantMessage.tsx +++ b/apps/web/src/components/AssistantMessage.tsx @@ -312,6 +312,11 @@ interface Props { ) => Promise<{ message?: string; url?: string } | void> | { message?: string; url?: string } | void; activePluginActionPaths?: Set; hiddenPluginActionPaths?: Set; + // Click handler for the "Share to Open Design" button rendered next to + // the post-completion Discord prompt. ProjectView wires this to + // handleSend with the bundled `od-share-to-community` trigger prompt. + onShareToOpenDesign?: () => void; + shareToOpenDesignBusy?: boolean; // True only for the most recent assistant message. isLast?: boolean; // Assistant message id whose run-failure error is rendered as ChatPane's @@ -369,6 +374,7 @@ const ASSISTANT_MESSAGE_COMPARED_PROPS: Array = [ 'errorCardOwnerId', 'nextUserContent', 'forking', + 'shareToOpenDesignBusy', 'suppressDirectionForms', 'hasDesignSystemContext', // Live streaming tool input changes identity on every `tool_input_delta`. @@ -414,6 +420,8 @@ function AssistantMessageImpl({ onRequestPluginFolderAgentAction, activePluginActionPaths = new Set(), hiddenPluginActionPaths = new Set(), + onShareToOpenDesign, + shareToOpenDesignBusy = false, isLast, errorCardOwnerId = null, nextUserContent, @@ -861,6 +869,28 @@ function AssistantMessageImpl({ isLast={!!isLast} /> )} + {/* + "Share to Open Design" — pairs with the post-feedback Discord + prompt (assistant-feedback-discord-note). Only shows on the most + recent assistant message after a successful run, gated on the + same isFeedbackEligible signal so it appears alongside the + thumbs-up/down + Discord CTA — not on errored runs, partial + streams, or empty responses. Click hands the user a packaged + plugin via the bundled od-share-to-community scenario. + */} + {onShareToOpenDesign && isLast && showFeedback ? ( + + ) : null} ) : null} diff --git a/apps/web/src/components/ChatPane.tsx b/apps/web/src/components/ChatPane.tsx index 9a610b331e..e203f0d279 100644 --- a/apps/web/src/components/ChatPane.tsx +++ b/apps/web/src/components/ChatPane.tsx @@ -466,6 +466,11 @@ interface Props { ) => Promise<{ message?: string; url?: string } | void> | { message?: string; url?: string } | void; activePluginActionPaths?: Set; hiddenPluginActionPaths?: Set; + // "Share to Open Design" button on each completed assistant message — + // wired by ProjectView to handleSend with the bundled + // `od-share-to-community` scenario's trigger prompt. + onShareToOpenDesign?: () => void; + shareToOpenDesignBusy?: boolean; forceStreamingMessageIds?: Set; // Live-only streaming tool-input partials keyed by tool-use id. Threaded to // AssistantMessage so an in-flight Write/Edit can render its code in real @@ -660,6 +665,8 @@ export function ChatPane({ onRequestPluginFolderAgentAction, activePluginActionPaths, hiddenPluginActionPaths, + onShareToOpenDesign, + shareToOpenDesignBusy, forceStreamingMessageIds, liveToolInput, initialDraft, @@ -767,6 +774,7 @@ export function ChatPane({ onArtifactShare, onArtifactChip, onForkFromMessage, + onShareToOpenDesign, }); assistantCallbacksRef.current = { onSubmitForm, @@ -775,6 +783,7 @@ export function ChatPane({ onArtifactShare, onArtifactChip, onForkFromMessage, + onShareToOpenDesign, }; const [tab, setTab] = useState('chat'); const [showConvList, setShowConvList] = useState(false); @@ -1923,6 +1932,8 @@ export function ChatPane({ onRequestPluginFolderAgentAction={onRequestPluginFolderAgentAction} activePluginActionPaths={activePluginActionPaths} hiddenPluginActionPaths={hiddenPluginActionPaths} + onShareToOpenDesign={onShareToOpenDesign} + shareToOpenDesignBusy={shareToOpenDesignBusy} forceStreamingMessageIds={forceStreamingMessageIds} lastAssistantId={lastAssistantId} firstUserMessageId={firstUserMessageId} @@ -2144,6 +2155,7 @@ interface AssistantCallbacks { onArtifactShare: ((fileName: string) => void) | undefined; onArtifactChip: ((fileName: string, prompt: string) => void) | undefined; onForkFromMessage: ((message: ChatMessage) => void) | undefined; + onShareToOpenDesign: (() => void) | undefined; } type ChatRenderItem = @@ -2192,6 +2204,8 @@ function ChatRows({ onRequestPluginFolderAgentAction, activePluginActionPaths, hiddenPluginActionPaths, + onShareToOpenDesign, + shareToOpenDesignBusy, forceStreamingMessageIds, lastAssistantId, firstUserMessageId, @@ -2227,6 +2241,8 @@ function ChatRows({ onRequestPluginFolderAgentAction?: (relativePath: string, action: PluginFolderAgentAction) => void; activePluginActionPaths?: Set; hiddenPluginActionPaths?: Set; + onShareToOpenDesign?: () => void; + shareToOpenDesignBusy?: boolean; forceStreamingMessageIds?: Set; lastAssistantId: string | undefined; firstUserMessageId: string | undefined; @@ -2309,6 +2325,12 @@ function ChatRows({ onRequestPluginFolderAgentAction={onRequestPluginFolderAgentAction} activePluginActionPaths={activePluginActionPaths} hiddenPluginActionPaths={hiddenPluginActionPaths} + onShareToOpenDesign={ + onShareToOpenDesign + ? () => assistantCallbacksRef.current.onShareToOpenDesign?.() + : undefined + } + shareToOpenDesignBusy={shareToOpenDesignBusy} isLast={m.id === lastAssistantId} errorCardOwnerId={errorCardOwnerId} nextUserContent={nextUserContentByAssistantId.get(m.id)} diff --git a/apps/web/src/components/ProjectView.tsx b/apps/web/src/components/ProjectView.tsx index 0ef83e8df4..7ba5df0829 100644 --- a/apps/web/src/components/ProjectView.tsx +++ b/apps/web/src/components/ProjectView.tsx @@ -180,6 +180,7 @@ import { FileWorkspace } from './FileWorkspace'; import { type PluginFolderAgentAction, } from './design-files/pluginFolderActions'; +import { SHARE_TO_COMMUNITY_PROMPT } from './share-to-community/shareToCommunityPrompt'; import { CenteredLoader } from './Loading'; import type { SettingsSection } from './SettingsDialog'; import { Toast } from './Toast'; @@ -4112,6 +4113,24 @@ export function ProjectView({ ], ); + // "Share to Open Design" — kicks off the bundled `od-share-to-community` + // scenario in the active conversation. We just inject the trigger prompt + // through the standard chat-send path; the agent then loads SKILL.md and + // drives the rest. Busy flag debounces the double-click while the send + // request is in flight (handleSend is async). + const [shareToOpenDesignBusy, setShareToOpenDesignBusy] = useState(false); + const shareToOpenDesignBusyRef = useRef(false); + const handleShareToOpenDesign = useCallback(() => { + if (currentConversationActionDisabled || shareToOpenDesignBusyRef.current) return; + shareToOpenDesignBusyRef.current = true; + setShareToOpenDesignBusy(true); + void Promise.resolve(handleSend(SHARE_TO_COMMUNITY_PROMPT, [], [])) + .finally(() => { + shareToOpenDesignBusyRef.current = false; + setShareToOpenDesignBusy(false); + }); + }, [currentConversationActionDisabled, handleSend]); + const sentDesignSystemReviewTaskKeysRef = useRef>(new Set()); const persistDesignSystemReviewEntry = useCallback(( sectionTitle: string, @@ -5295,6 +5314,8 @@ export function ProjectView({ onRequestPluginFolderAgentAction={handlePluginFolderAgentAction} activePluginActionPaths={activePluginActionPaths} hiddenPluginActionPaths={hiddenAssistantPluginActionPaths} + onShareToOpenDesign={handleShareToOpenDesign} + shareToOpenDesignBusy={shareToOpenDesignBusy} forceStreamingMessageIds={forceStreamingPluginMessageIds} initialDraft={chatInitialDraft} onSubmitForm={(text) => { diff --git a/apps/web/src/components/share-to-community/shareToCommunityPrompt.ts b/apps/web/src/components/share-to-community/shareToCommunityPrompt.ts new file mode 100644 index 0000000000..0501d84707 --- /dev/null +++ b/apps/web/src/components/share-to-community/shareToCommunityPrompt.ts @@ -0,0 +1,36 @@ +// Trigger prompt for the "Share to Open Design" button next to the +// post-completion Discord prompt. Mirrors the constraint surface of +// `home-hero/plugin-authoring.ts`'s `PLUGIN_AUTHORING_PROMPT_TEMPLATE` +// (no chained shell suggestions, no jq, point user at the plugin-folder +// card buttons after the validate/install step) — but starts from +// "introspect the project the user just finished" instead of from a +// goal string the user typed. +// +// The bundled scenario at `plugins/_official/scenarios/od-share-to-community/` +// owns the full playbook. This prompt is the kickoff message that lands in +// the conversation when the user clicks the button. + +export const SHARE_TO_COMMUNITY_PROMPT = [ + 'Package the work I just finished in this project as an Open Design plugin so I can install it into My plugins and open a PR to the Open Design community catalog.', + '', + 'Follow the bundled `od-share-to-community` scenario at `plugins/_official/scenarios/od-share-to-community/SKILL.md` end to end. Do not ask me for fields the project files already answer — read `*.artifact.json`, `brand-spec.md` / `DESIGN.md`, and the generated artifacts in this workspace, then derive the manifest from what you find.', + '', + 'Produce a folder named `generated-plugin/` with:', + '- SKILL.md describing the agent behavior and workflow', + '- open-design.json with valid metadata: specVersion, name, version, description, mode, task kind, inputs, plus any pipeline / context references the workflow needs', + '- plugin.repo is optional during scaffolding, but do not silently omit it: check `gh --version` and `gh auth status`, then prefer the local account login printed by auth status. Only use `gh api user --jq .login` as a fallback when auth status does not expose a login. If `gh` is missing, not logged in, rate-limited, or cannot resolve a real owner, omit plugin.repo instead of inventing an owner and explicitly report the auth problem with `gh auth refresh -h github.com -s repo,workflow`, `gh auth login -h github.com -s repo,workflow`, or `od plugin publish-repo generated-plugin --owner ` as recovery commands. Never write placeholder owners such as `open-design-user`, ``, `example-user`, `your-org`, or `your-username` into the final manifest.', + '- optional examples/ and assets/ when useful (copy the most recent generated artifact from this project into `generated-plugin/examples/` and reference it from the manifest)', + '', + 'Validate the plugin locally before reporting: run `od plugin validate` on the folder, then `od plugin pack` for a tarball, then `od plugin install --source ` to confirm the install path works.', + '', + 'When the work above is done, write a single summary turn covering: files created, `od plugin validate` status, local install / run status, and `od plugin pack` output. Then STOP.', + '', + '**Do NOT** suggest follow-up CLI commands such as `od plugin publish`, `od plugin publish --to open-design`, `gh repo create`, `git init` / `git remote add` / `git push`, or any other publish / repo wiring. The plugin-folder card under Design Files already exposes three buttons whose prompts drive those flows end-to-end with the right auth gates, fallbacks, and retry rules baked in:', + '- **Add to My plugins** — already satisfied by this turn\'s `od plugin install --source` step.', + '- **Publish repo** — creates / updates the author\'s `plugin.repo` GitHub repo through a gh + git sequence the agent is told exactly how to run.', + '- **Open Design PR** — opens a draft PR against `nexu-io/open-design` for the community catalog.', + '', + 'Point the user at whichever button they want next; do NOT recreate those flows as freeform shell suggestions in this summary. Recreating them drifts from the button prompts\' guarantees and is the source of the bug that closed #2332.', + '', + '**Do NOT** assume the standalone `jq` binary is installed (it is not part of the OD agent runtime baseline and is missing from default macOS / Windows shells). When you need to read the manifest, prefer your built-in file-reading tool, then `cat generated-plugin/open-design.json` followed by manual JSON parsing, then `node -e \'console.log(JSON.parse(require("fs").readFileSync("generated-plugin/open-design.json","utf8")))\'`. The `gh ... --jq` flag is fine because gh ships its own embedded library; the brew-installed standalone `jq` is NOT.', +].join('\n'); diff --git a/apps/web/src/i18n/locales/ar.ts b/apps/web/src/i18n/locales/ar.ts index 06fde30ff0..858d756cd8 100644 --- a/apps/web/src/i18n/locales/ar.ts +++ b/apps/web/src/i18n/locales/ar.ts @@ -1430,6 +1430,8 @@ export const ar: Dict = { 'assistant.copyMarkdown': 'نسخ Markdown الرد', 'assistant.forkConversation': 'تفرع من هنا', 'assistant.forkingConversation': 'جارٍ التفرع…', + 'assistant.shareToOpenDesign': 'المشاركة على Open Design', + 'assistant.shareToOpenDesignBusy': 'جارٍ المشاركة…', 'assistant.feedbackPrompt': 'ملاحظات', 'assistant.feedbackPositive': 'مفيد', 'assistant.feedbackNegative': 'غير مفيد', diff --git a/apps/web/src/i18n/locales/de.ts b/apps/web/src/i18n/locales/de.ts index a764e03cab..083623e8af 100644 --- a/apps/web/src/i18n/locales/de.ts +++ b/apps/web/src/i18n/locales/de.ts @@ -1318,6 +1318,8 @@ export const de: Dict = { 'assistant.copyMarkdown': 'Antwort-Markdown kopieren', 'assistant.forkConversation': 'Ab hier forken', 'assistant.forkingConversation': 'Fork wird erstellt…', + 'assistant.shareToOpenDesign': 'An Open Design teilen', + 'assistant.shareToOpenDesignBusy': 'Wird geteilt…', 'assistant.feedbackPrompt': 'Feedback', 'assistant.feedbackPositive': 'Hilfreich', 'assistant.feedbackNegative': 'Nicht hilfreich', diff --git a/apps/web/src/i18n/locales/en.ts b/apps/web/src/i18n/locales/en.ts index 35a7581dc4..0a4fb85352 100644 --- a/apps/web/src/i18n/locales/en.ts +++ b/apps/web/src/i18n/locales/en.ts @@ -2386,6 +2386,8 @@ export const en: Dict = { 'assistant.copyMarkdown': 'Copy response markdown', 'assistant.forkConversation': 'Fork from here', 'assistant.forkingConversation': 'Forking…', + 'assistant.shareToOpenDesign': 'Share to Open Design', + 'assistant.shareToOpenDesignBusy': 'Sharing…', 'assistant.feedbackPrompt': 'Feedback', 'assistant.feedbackPositive': 'Helpful', 'assistant.feedbackNegative': 'Not helpful', diff --git a/apps/web/src/i18n/locales/es-ES.ts b/apps/web/src/i18n/locales/es-ES.ts index 941f037222..12604c22d5 100644 --- a/apps/web/src/i18n/locales/es-ES.ts +++ b/apps/web/src/i18n/locales/es-ES.ts @@ -1319,6 +1319,8 @@ export const esES: Dict = { 'assistant.copyMarkdown': 'Copiar Markdown de la respuesta', 'assistant.forkConversation': 'Bifurcar desde aquí', 'assistant.forkingConversation': 'Bifurcando…', + 'assistant.shareToOpenDesign': 'Compartir con Open Design', + 'assistant.shareToOpenDesignBusy': 'Compartiendo…', 'assistant.feedbackPrompt': 'Comentarios', 'assistant.feedbackPositive': 'Útil', 'assistant.feedbackNegative': 'No útil', diff --git a/apps/web/src/i18n/locales/fa.ts b/apps/web/src/i18n/locales/fa.ts index 45d5e72095..d344abaa9c 100644 --- a/apps/web/src/i18n/locales/fa.ts +++ b/apps/web/src/i18n/locales/fa.ts @@ -1411,6 +1411,8 @@ export const fa: Dict = { 'assistant.copyMarkdown': 'کپی Markdown پاسخ', 'assistant.forkConversation': 'فورک از اینجا', 'assistant.forkingConversation': 'در حال فورک…', + 'assistant.shareToOpenDesign': 'اشتراک‌گذاری در Open Design', + 'assistant.shareToOpenDesignBusy': 'در حال اشتراک‌گذاری…', 'assistant.feedbackPrompt': 'بازخورد', 'assistant.feedbackPositive': 'مفید', 'assistant.feedbackNegative': 'غیرمفید', diff --git a/apps/web/src/i18n/locales/fr.ts b/apps/web/src/i18n/locales/fr.ts index 11d150a8da..e6ee3f90eb 100644 --- a/apps/web/src/i18n/locales/fr.ts +++ b/apps/web/src/i18n/locales/fr.ts @@ -2034,6 +2034,8 @@ export const fr: Dict = { 'assistant.copyMarkdown': 'Copier le Markdown de la réponse', 'assistant.forkConversation': 'Forker depuis ici', 'assistant.forkingConversation': 'Fork en cours…', + 'assistant.shareToOpenDesign': 'Partager à Open Design', + 'assistant.shareToOpenDesignBusy': 'Partage en cours…', 'assistant.feedbackPrompt': 'Avis', 'assistant.feedbackPositive': 'Utile', 'assistant.feedbackNegative': 'Pas utile', diff --git a/apps/web/src/i18n/locales/hu.ts b/apps/web/src/i18n/locales/hu.ts index 93c03d455e..ba812fb893 100644 --- a/apps/web/src/i18n/locales/hu.ts +++ b/apps/web/src/i18n/locales/hu.ts @@ -1430,6 +1430,8 @@ export const hu: Dict = { 'assistant.copyMarkdown': 'Válasz Markdown másolása', 'assistant.forkConversation': 'Fork innen', 'assistant.forkingConversation': 'Fork létrehozása…', + 'assistant.shareToOpenDesign': 'Megosztás az Open Designnal', + 'assistant.shareToOpenDesignBusy': 'Megosztás folyamatban…', 'assistant.feedbackPrompt': 'Visszajelzés', 'assistant.feedbackPositive': 'Hasznos', 'assistant.feedbackNegative': 'Nem hasznos', diff --git a/apps/web/src/i18n/locales/id.ts b/apps/web/src/i18n/locales/id.ts index b5b1d79454..feddc3470f 100644 --- a/apps/web/src/i18n/locales/id.ts +++ b/apps/web/src/i18n/locales/id.ts @@ -1579,6 +1579,8 @@ export const id: Dict = { 'assistant.copyMarkdown': 'Salin Markdown respons', 'assistant.forkConversation': 'Fork dari sini', 'assistant.forkingConversation': 'Membuat fork…', + 'assistant.shareToOpenDesign': 'Bagikan ke Open Design', + 'assistant.shareToOpenDesignBusy': 'Membagikan…', 'assistant.feedbackPrompt': 'Masukan', 'assistant.feedbackPositive': 'Membantu', 'assistant.feedbackNegative': 'Tidak membantu', diff --git a/apps/web/src/i18n/locales/it.ts b/apps/web/src/i18n/locales/it.ts index b8449f5926..9b34ac5f00 100644 --- a/apps/web/src/i18n/locales/it.ts +++ b/apps/web/src/i18n/locales/it.ts @@ -1310,6 +1310,8 @@ export const it: Dict = { 'assistant.copyMarkdown': 'Copia il Markdown della risposta', 'assistant.forkConversation': 'Fork da qui', 'assistant.forkingConversation': 'Creazione fork…', + 'assistant.shareToOpenDesign': 'Condividi su Open Design', + 'assistant.shareToOpenDesignBusy': 'Condivisione…', 'assistant.emptyResponseLabel': 'Nessun output', 'assistant.emptyResponseMessage': 'Il provider ha terminato la richiesta senza restituire testo o un artefatto. Prova un altro modello o provider, controlla la quota o riprova.', 'assistant.unfinishedLabel': 'Fermato con lavoro non completato', diff --git a/apps/web/src/i18n/locales/ja.ts b/apps/web/src/i18n/locales/ja.ts index 71a628cf57..5bb7fb21bc 100644 --- a/apps/web/src/i18n/locales/ja.ts +++ b/apps/web/src/i18n/locales/ja.ts @@ -1317,6 +1317,8 @@ export const ja: Dict = { 'assistant.copyMarkdown': '応答のMarkdownをコピー', 'assistant.forkConversation': 'ここからフォーク', 'assistant.forkingConversation': 'フォーク中…', + 'assistant.shareToOpenDesign': 'Open Design に共有', + 'assistant.shareToOpenDesignBusy': '共有中…', 'assistant.feedbackPrompt': 'フィードバック', 'assistant.feedbackPositive': '役に立った', 'assistant.feedbackNegative': '役に立たなかった', diff --git a/apps/web/src/i18n/locales/ko.ts b/apps/web/src/i18n/locales/ko.ts index d2c19f966e..58c36d7197 100644 --- a/apps/web/src/i18n/locales/ko.ts +++ b/apps/web/src/i18n/locales/ko.ts @@ -1430,6 +1430,8 @@ export const ko: Dict = { 'assistant.copyMarkdown': '응답 Markdown 복사', 'assistant.forkConversation': '여기서 포크', 'assistant.forkingConversation': '포크 중…', + 'assistant.shareToOpenDesign': 'Open Design에 공유', + 'assistant.shareToOpenDesignBusy': '공유 중…', 'assistant.feedbackPrompt': '피드백', 'assistant.feedbackPositive': '도움 됨', 'assistant.feedbackNegative': '도움 안 됨', diff --git a/apps/web/src/i18n/locales/pl.ts b/apps/web/src/i18n/locales/pl.ts index ac720d7129..97b9cd367d 100644 --- a/apps/web/src/i18n/locales/pl.ts +++ b/apps/web/src/i18n/locales/pl.ts @@ -1430,6 +1430,8 @@ export const pl: Dict = { 'assistant.copyMarkdown': 'Kopiuj Markdown odpowiedzi', 'assistant.forkConversation': 'Fork od tego miejsca', 'assistant.forkingConversation': 'Tworzenie forka…', + 'assistant.shareToOpenDesign': 'Udostępnij Open Design', + 'assistant.shareToOpenDesignBusy': 'Udostępnianie…', 'assistant.feedbackPrompt': 'Opinia', 'assistant.feedbackPositive': 'Pomocne', 'assistant.feedbackNegative': 'Niepomocne', diff --git a/apps/web/src/i18n/locales/pt-BR.ts b/apps/web/src/i18n/locales/pt-BR.ts index 5bd708bded..f71b3a95c7 100644 --- a/apps/web/src/i18n/locales/pt-BR.ts +++ b/apps/web/src/i18n/locales/pt-BR.ts @@ -1471,6 +1471,8 @@ export const ptBR: Dict = { 'assistant.copyMarkdown': 'Copiar Markdown da resposta', 'assistant.forkConversation': 'Bifurcar daqui', 'assistant.forkingConversation': 'Bifurcando…', + 'assistant.shareToOpenDesign': 'Compartilhar com Open Design', + 'assistant.shareToOpenDesignBusy': 'Compartilhando…', 'assistant.feedbackPrompt': 'Feedback', 'assistant.feedbackPositive': 'Útil', 'assistant.feedbackNegative': 'Não útil', diff --git a/apps/web/src/i18n/locales/ru.ts b/apps/web/src/i18n/locales/ru.ts index 04874bae3e..0b93fc444f 100644 --- a/apps/web/src/i18n/locales/ru.ts +++ b/apps/web/src/i18n/locales/ru.ts @@ -1471,6 +1471,8 @@ export const ru: Dict = { 'assistant.copyMarkdown': 'Скопировать Markdown ответа', 'assistant.forkConversation': 'Создать форк отсюда', 'assistant.forkingConversation': 'Создание форка…', + 'assistant.shareToOpenDesign': 'Поделиться в Open Design', + 'assistant.shareToOpenDesignBusy': 'Делимся…', 'assistant.feedbackPrompt': 'Отзыв', 'assistant.feedbackPositive': 'Полезно', 'assistant.feedbackNegative': 'Не полезно', diff --git a/apps/web/src/i18n/locales/th.ts b/apps/web/src/i18n/locales/th.ts index ab23e6505b..8780b80161 100644 --- a/apps/web/src/i18n/locales/th.ts +++ b/apps/web/src/i18n/locales/th.ts @@ -1389,6 +1389,8 @@ export const th: Dict = { 'assistant.copyMarkdown': 'คัดลอก Markdown ของคำตอบ', 'assistant.forkConversation': 'Fork จากตรงนี้', 'assistant.forkingConversation': 'กำลัง fork…', + 'assistant.shareToOpenDesign': 'แชร์ไปยัง Open Design', + 'assistant.shareToOpenDesignBusy': 'กำลังแชร์…', 'assistant.feedbackPrompt': 'ข้อเสนอแนะ', 'assistant.feedbackPositive': 'มีประโยชน์', 'assistant.feedbackNegative': 'ไม่มีประโยชน์', diff --git a/apps/web/src/i18n/locales/tr.ts b/apps/web/src/i18n/locales/tr.ts index 55d98bd70d..261b6a0c4b 100644 --- a/apps/web/src/i18n/locales/tr.ts +++ b/apps/web/src/i18n/locales/tr.ts @@ -1417,6 +1417,8 @@ export const tr: Dict = { 'assistant.copyMarkdown': 'Yanıt Markdown\'unu kopyala', 'assistant.forkConversation': 'Buradan fork et', 'assistant.forkingConversation': 'Fork ediliyor…', + 'assistant.shareToOpenDesign': "Open Design'a paylaş", + 'assistant.shareToOpenDesignBusy': 'Paylaşılıyor…', 'assistant.feedbackPrompt': 'Geri bildirim', 'assistant.feedbackPositive': 'Yararlı', 'assistant.feedbackNegative': 'Yararlı değil', diff --git a/apps/web/src/i18n/locales/uk.ts b/apps/web/src/i18n/locales/uk.ts index 2315a3819b..d029f65318 100644 --- a/apps/web/src/i18n/locales/uk.ts +++ b/apps/web/src/i18n/locales/uk.ts @@ -1472,6 +1472,8 @@ export const uk: Dict = { 'assistant.copyMarkdown': 'Скопіювати Markdown відповіді', 'assistant.forkConversation': 'Створити форк звідси', 'assistant.forkingConversation': 'Створення форка…', + 'assistant.shareToOpenDesign': 'Поділитися з Open Design', + 'assistant.shareToOpenDesignBusy': 'Ділимося…', 'assistant.feedbackPrompt': 'Відгук', 'assistant.feedbackPositive': 'Корисно', 'assistant.feedbackNegative': 'Не корисно', diff --git a/apps/web/src/i18n/locales/zh-CN.ts b/apps/web/src/i18n/locales/zh-CN.ts index 1f059c9935..10d7dfb7a2 100644 --- a/apps/web/src/i18n/locales/zh-CN.ts +++ b/apps/web/src/i18n/locales/zh-CN.ts @@ -2336,6 +2336,8 @@ export const zhCN: Dict = { 'assistant.copyMarkdown': '复制回复 Markdown', 'assistant.forkConversation': '从这里分叉', 'assistant.forkingConversation': '正在分叉…', + 'assistant.shareToOpenDesign': '分享到 Open Design', + 'assistant.shareToOpenDesignBusy': '分享中…', 'assistant.feedbackPrompt': '反馈', 'assistant.feedbackPositive': '有帮助', 'assistant.feedbackNegative': '没有帮助', diff --git a/apps/web/src/i18n/locales/zh-TW.ts b/apps/web/src/i18n/locales/zh-TW.ts index 3a2f7859f7..8211b0e4a9 100644 --- a/apps/web/src/i18n/locales/zh-TW.ts +++ b/apps/web/src/i18n/locales/zh-TW.ts @@ -1881,6 +1881,8 @@ export const zhTW: Dict = { 'assistant.copyMarkdown': '複製回覆 Markdown', 'assistant.forkConversation': '從這裡分叉', 'assistant.forkingConversation': '正在分叉…', + 'assistant.shareToOpenDesign': '分享到 Open Design', + 'assistant.shareToOpenDesignBusy': '分享中…', 'assistant.feedbackPrompt': '意見回饋', 'assistant.feedbackPositive': '有幫助', 'assistant.feedbackNegative': '沒有幫助', diff --git a/apps/web/src/i18n/types.ts b/apps/web/src/i18n/types.ts index f489e54892..b95a7ba0be 100644 --- a/apps/web/src/i18n/types.ts +++ b/apps/web/src/i18n/types.ts @@ -2717,6 +2717,8 @@ export interface Dict { 'assistant.copyMarkdown': string; 'assistant.forkConversation': string; 'assistant.forkingConversation': string; + 'assistant.shareToOpenDesign': string; + 'assistant.shareToOpenDesignBusy': string; 'assistant.feedbackPrompt': string; 'assistant.feedbackPositive': string; 'assistant.feedbackNegative': string; diff --git a/apps/web/src/styles/viewer/composio.css b/apps/web/src/styles/viewer/composio.css index eb4b77d1af..f6f762c1e1 100644 --- a/apps/web/src/styles/viewer/composio.css +++ b/apps/web/src/styles/viewer/composio.css @@ -2481,6 +2481,35 @@ body.entry-resizing { cursor: col-resize; user-select: none; } align-items: flex-start; gap: 6px; } +/* + * "Share to Open Design" button — sits inside the completion row, below + * the AssistantFeedback / AssistantFooter widgets so it visually pairs + * with the post-thumbs-up Discord note (`assistant-feedback-discord-note`) + * that appears nearby. Styled as a quiet pill so it doesn't compete with + * the in-message tool-card buttons or the primary chat composer. + */ +.assistant-share-to-od-btn { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 12px; + background: var(--bg-subtle); + border: 1px solid var(--border); + border-radius: var(--radius-pill); + font-size: 12px; + color: var(--text-default); + cursor: pointer; + transition: background 140ms cubic-bezier(0.23, 1, 0.32, 1), + border-color 140ms cubic-bezier(0.23, 1, 0.32, 1); +} +.assistant-share-to-od-btn:hover:not(:disabled) { + background: var(--bg-default); + border-color: var(--border-strong); +} +.assistant-share-to-od-btn:disabled { + opacity: 0.6; + cursor: not-allowed; +} .assistant-footer { display: inline-flex; align-items: center; diff --git a/apps/web/tests/components/AssistantMessage.test.tsx b/apps/web/tests/components/AssistantMessage.test.tsx index 46415e17dd..85a5bec7f8 100644 --- a/apps/web/tests/components/AssistantMessage.test.tsx +++ b/apps/web/tests/components/AssistantMessage.test.tsx @@ -7,6 +7,7 @@ */ import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { useState } from 'react'; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; import { AssistantMessage } from '../../src/components/AssistantMessage'; @@ -118,6 +119,38 @@ describe('AssistantMessage feedback gate', () => { expect(onForkFromMessage).toHaveBeenCalledTimes(1); }); + it('disables the Share to Open Design button before a duplicate click can reuse stale props', () => { + const onShare = vi.fn(); + + function Harness() { + const [busy, setBusy] = useState(false); + return ( + { + if (busy) return; + onShare(); + setBusy(true); + }} + shareToOpenDesignBusy={busy} + /> + ); + } + + render(); + + fireEvent.click(screen.getByTestId('assistant-share-to-od')); + expect(screen.getByTestId('assistant-share-to-od').disabled).toBe(true); + fireEvent.click(screen.getByTestId('assistant-share-to-od')); + + expect(onShare).toHaveBeenCalledTimes(1); + expect(screen.getByRole('button', { name: 'Sharing…' })).toBeTruthy(); + }); + it('does not show the fork action while the assistant is streaming', () => { render( ` as recovery commands. Never write placeholder owners such as `open-design-user`, ``, `example-user`, `your-org`, or `your-username` into the final manifest. +- Optional `examples/` and `assets/` only when they help review or reuse. + +## Auto-derive from the project — do not ask the user fields the files already answer + +The agent's CWD is the user's OD project root. Before generating anything, **read what's already there** and infer the plugin from it. Treat the project files as the source of truth; the user does not need to retype things you can see. + +What to read, in this order, and what to take from each: + +1. `*.artifact.json` (or `artifact.json`) at the project root — task kind, mode, the prompt the user actually ran, the file the agent produced. This drives `od.taskKind`, `od.mode`, the default `useCase.query`, and the example output path. +2. `brand-spec.md` / `DESIGN.md` if present — voice, brand name, audience. Folds into the plugin description and tags. +3. The list of generated artifacts in the project workspace (the `*.html`, `*.tsx`, `*.svg`, etc. the agent wrote during this session) — pick the most recent / largest one as `useCase.exampleOutputs[0].path` after copying it under `generated-plugin/examples/`. +4. The user's first prompt in this conversation, if surfaced via the runtime — the natural-language description of what they wanted. Folds into `description` and the default `useCase.query`. + +Pick a stable plugin id from what you derived: lowercase letters, numbers, dashes, underscores, dots. Prefer something the brand-spec or artifact metadata suggests over inventing one. + +If a field truly cannot be derived (e.g. no artifact.json exists, no brand-spec, the project is too sparse), only then ask the user — and ask in **one** consolidated `AskUserQuestion`, not field-by-field. Default the answers from whatever you did manage to derive so the user can accept by clicking through. + +## Validate the plugin locally before reporting + +Run `od plugin validate` on the folder, then `od plugin pack` for a tarball, then `od plugin install --source ` to confirm the install path works. + +## When the work above is done + +Write a single summary turn covering: files created, `od plugin validate` status, local install / run status, and `od plugin pack` output. Then STOP. + +## Do NOT chain the publish-repo / Open-Design-PR flows yourself + +Do NOT suggest follow-up CLI commands such as `od plugin publish`, `od plugin publish --to open-design`, `gh repo create`, `git init` / `git remote add` / `git push`, or any other publish / repo wiring. The plugin-folder card under Design Files already exposes three buttons whose prompts drive those flows end-to-end with the right auth gates, fallbacks, and retry rules baked in: + +- **Add to My plugins** — already satisfied by this turn's `od plugin install --source` step. +- **Publish repo** — creates / updates the author's `plugin.repo` GitHub repo through a gh + git sequence the agent is told exactly how to run. +- **Open Design PR** — opens a draft PR against `nexu-io/open-design` for the community catalog. + +Point the user at whichever button they want next; do NOT recreate those flows as freeform shell suggestions in this summary. Recreating them drifts from the button prompts' guarantees and is the source of the bug that closed #2332. + +## Do NOT assume `jq` is on PATH + +Do NOT assume the standalone `jq` binary is installed (it is not part of the OD agent runtime baseline and is missing from default macOS / Windows shells). When you need to read the manifest, prefer your built-in file-reading tool, then `cat generated-plugin/open-design.json` followed by manual JSON parsing, then `node -e 'console.log(JSON.parse(require("fs").readFileSync("generated-plugin/open-design.json","utf8")))'`. The `gh ... --jq` flag is fine because gh ships its own embedded library; the brew-installed standalone `jq` is NOT. + +## Language + +Mirror the user's chat language in any `AskUserQuestion` labels, status updates, and error explanations. Generated artifacts (manifest fields, SKILL.md body, PR / commit messages, branch names) MUST stay English regardless of the chat language — that's the OD plugins-spec convention and matches the existing scenarios under `plugins/_official/scenarios/`. + +## Suggested folder shape + +```text +generated-plugin/ + SKILL.md + open-design.json + examples/ + + assets/ + +``` + +## Spec references + +- `docs/plugins-spec.md` +- `docs/schemas/open-design.plugin.v1.json` +- The sibling `plugins/_official/scenarios/od-plugin-authoring/SKILL.md` for the from-scratch authoring counterpart. diff --git a/plugins/_official/scenarios/od-share-to-community/open-design.json b/plugins/_official/scenarios/od-share-to-community/open-design.json new file mode 100644 index 0000000000..e2d1fd0f62 --- /dev/null +++ b/plugins/_official/scenarios/od-share-to-community/open-design.json @@ -0,0 +1,85 @@ +{ + "$schema": "https://open-design.ai/schemas/plugin.v1.json", + "specVersion": "1.0.0", + "name": "od-share-to-community", + "title": "Share to community", + "title_i18n": { + "en": "Share to community", + "zh-CN": "分享到社区", + "zh-TW": "分享到社群", + "ja": "コミュニティへ共有", + "ko": "커뮤니티에 공유", + "de": "An die Community teilen", + "fr": "Partager à la communauté", + "ru": "Поделиться с сообществом", + "es": "Compartir con la comunidad", + "pt-BR": "Compartilhar com a comunidade", + "vi": "Chia sẻ với cộng đồng", + "id": "Bagikan ke komunitas", + "ar": "المشاركة مع المجتمع", + "tr": "Topluluğa paylaş", + "uk": "Поділитися зі спільнотою" + }, + "version": "0.1.0", + "description": "Package the work the user just finished into an Open Design plugin and route them to the existing Add-to-My-plugins / Open-Design-PR buttons. Triggered by the 'Share to Open Design' button next to the post-completion Discord prompt.", + "description_i18n": { + "en": "Package the work the user just finished into an Open Design plugin and route them to the existing Add-to-My-plugins / Open-Design-PR buttons. Triggered by the 'Share to Open Design' button next to the post-completion Discord prompt.", + "zh-CN": "把用户刚做完的作品打包成 Open Design 插件,然后引导他们点已有的 Add to My plugins / Open Design PR 按钮。在生成完成后 Discord 提示旁边的 'Share to Open Design' 按钮触发。" + }, + "license": "MIT", + "author": { + "name": "Open Design", + "url": "https://github.com/nexu-io" + }, + "homepage": "https://github.com/nexu-io/open-design/tree/main/plugins/_official/scenarios/od-share-to-community", + "tags": [ + "scenario", + "first-party", + "plugin-authoring", + "share", + "contribute" + ], + "compat": { + "agentSkills": [ + { "path": "./SKILL.md" } + ] + }, + "od": { + "kind": "scenario", + "taskKind": "new-generation", + "scenario": "plugin-sharing", + "mode": "scenario", + "context": { + "skills": [ + { "path": "./SKILL.md" } + ] + }, + "pipeline": { + "stages": [ + { + "id": "inspect-project", + "atoms": [ + "file-read" + ] + }, + { + "id": "package-plugin", + "atoms": [ + "file-write" + ] + } + ] + }, + "capabilities": [ + "prompt:inject", + "fs:read", + "fs:write" + ], + "useCase": { + "query": { + "en": "Package the work I just finished in this project as an Open Design plugin so I can install it into My plugins and open a PR to the Open Design community catalog. Auto-derive the manifest from what's already in the project — don't ask me for fields the project files already answer.", + "zh-CN": "把我在这个 project 里刚做完的作品打包成一个 Open Design 插件,让我可以安装到 My plugins 并提交 PR 到 Open Design 社区。manifest 从项目里已有的内容自动推断,别问我已经能从文件里看出来的字段。" + } + } + } +}