feat(scenarios): od-share-to-community skill + post-completion Share button (#3657)

* feat(scenarios): add od-share-to-community plugin + Share to Open Design button

After the user finishes a generation, they currently have to re-prompt the
agent to package what they just made. This adds a quiet "Share to Open
Design" pill in the assistant-completion-row — same area as the
post-feedback Discord prompt — that injects a single trigger prompt
into the chat and lets the bundled `od-share-to-community` scenario
auto-derive the manifest from `*.artifact.json`, `brand-spec.md`, and
the workspace files instead of asking the user fields the project
files already answer.

The skill mirrors `home-hero/plugin-authoring.ts`'s constraint surface
verbatim: no chained CLI suggestions, no `jq` dependency (not in the
agent runtime baseline), and after `od plugin install --source`, point
the user at the existing 3-button plugin-folder card (Add to My plugins
/ Publish repo / Open Design PR) instead of recreating those flows
freeform — the source of bug #2332.

Verified end-to-end via headless browser:
- daemon registers the bundled scenario at /api/plugins
- button renders in the live DOM, gated on (isLast && showFeedback)
- click fires POST /api/runs with the trigger prompt as a user message
- Claude Opus picks up the prompt and starts the share flow

* fix(web): debounce share to open design action

* fix(plugins): register share community scenario pipeline

---------

Co-authored-by: kokisanai <kokisanai@users.noreply.github.com>
Co-authored-by: Siri-Ray <2667192167@qq.com>
This commit is contained in:
kokisanai
2026-06-05 08:09:04 +00:00
committed by GitHub
co-authored by kokisanai Siri-Ray
parent 3c1a8e7bcd
commit 2569f4eba9
29 changed files with 376 additions and 0 deletions
@@ -35,6 +35,7 @@ const SIBLINGS = new Map<string, { taskKind: string }>([
['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' }],
@@ -312,6 +312,11 @@ interface Props {
) => Promise<{ message?: string; url?: string } | void> | { message?: string; url?: string } | void;
activePluginActionPaths?: Set<string>;
hiddenPluginActionPaths?: Set<string>;
// 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<keyof Props> = [
'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 ? (
<button
type="button"
className="assistant-share-to-od-btn"
data-testid="assistant-share-to-od"
disabled={shareToOpenDesignBusy}
onClick={onShareToOpenDesign}
>
{shareToOpenDesignBusy
? t('assistant.shareToOpenDesignBusy')
: t('assistant.shareToOpenDesign')}
</button>
) : null}
</div>
) : null}
</div>
+22
View File
@@ -466,6 +466,11 @@ interface Props {
) => Promise<{ message?: string; url?: string } | void> | { message?: string; url?: string } | void;
activePluginActionPaths?: Set<string>;
hiddenPluginActionPaths?: Set<string>;
// "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<string>;
// 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<Tab>('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<string>;
hiddenPluginActionPaths?: Set<string>;
onShareToOpenDesign?: () => void;
shareToOpenDesignBusy?: boolean;
forceStreamingMessageIds?: Set<string>;
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)}
+21
View File
@@ -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<Set<string>>(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) => {
@@ -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 <github-login-or-org>` as recovery commands. Never write placeholder owners such as `open-design-user`, `<vendor>`, `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 <absolute-folder-path>` 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');
+2
View File
@@ -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': 'غير مفيد',
+2
View File
@@ -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',
+2
View File
@@ -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',
+2
View File
@@ -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',
+2
View File
@@ -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': 'غیرمفید',
+2
View File
@@ -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',
+2
View File
@@ -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',
+2
View File
@@ -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',
+2
View File
@@ -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',
+2
View File
@@ -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': '役に立たなかった',
+2
View File
@@ -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': '도움 안 됨',
+2
View File
@@ -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',
+2
View File
@@ -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',
+2
View File
@@ -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': 'Не полезно',
+2
View File
@@ -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': 'ไม่มีประโยชน์',
+2
View File
@@ -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',
+2
View File
@@ -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': 'Не корисно',
+2
View File
@@ -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': '没有帮助',
+2
View File
@@ -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': '沒有幫助',
+2
View File
@@ -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;
+29
View File
@@ -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;
@@ -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 (
<AssistantMessage
message={baseMessage()}
streaming={false}
projectId="proj-1"
isLast
onFeedback={vi.fn()}
onShareToOpenDesign={() => {
if (busy) return;
onShare();
setBusy(true);
}}
shareToOpenDesignBusy={busy}
/>
);
}
render(<Harness />);
fireEvent.click(screen.getByTestId('assistant-share-to-od'));
expect(screen.getByTestId<HTMLButtonElement>('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(
<AssistantMessage
@@ -0,0 +1,79 @@
---
name: od-share-to-community
description: Package the user's just-finished work as an Open Design plugin without asking for fields the project files already answer, then surface the existing Add-to-My-plugins / Open-Design-PR buttons.
od:
scenario: plugin-sharing
mode: scenario
---
# od-share-to-community (scenario)
Triggered by the "Share to Open Design" button that sits next to the post-completion Discord prompt. The user just finished a piece of work in this project and wants to ship it as a plugin. They have not been asked any questions yet.
## Required outcome
Produce a folder named `generated-plugin/` in the active project workspace. At minimum:
- `SKILL.md` with frontmatter and clear agent instructions.
- `open-design.json` with valid plugin 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 <github-login-or-org>` as recovery commands. Never write placeholder owners such as `open-design-user`, `<vendor>`, `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 <absolute-folder-path>` 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/
<copied-from-the-project>
assets/
<if-needed>
```
## 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.
@@ -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 从项目里已有的内容自动推断,别问我已经能从文件里看出来的字段。"
}
}
}
}