✨ feat(vscode): 工单挂原型插件,详情面板一键导入 Figma

- prototypeFile 走通 state → Issue → webview 全链路(gitea/youtrack 两个
  loader、两份 KNOWN_STATE_FIELDS、issue/patch、webview types),按 planFile
  同款当共享字段;路径校验换成 .json 版,允许绝对路径
- 详情面板「计划文件」下加「原型插件路径」行:双击开 manifest.json,
  行尾按钮「在 Figma 中导入并打开」;没挂插件时只显示空态
- src/figma/:把 manifest 写进 Figma 桌面版的 localFileExtensions 再拉起
  Figma。坑是 Figma 退出会用内存状态覆盖 settings.json,所以写之前先
  osascript 优雅退出并轮询等它真没了(10s 等不到就让用户手动关,不强杀);
  写前备份 settings.json.bak-<时间戳>;已导入过就跳过写入直接打开;
  非 macOS 只把路径复制到剪贴板
- webhook 里 spec/plan 的 marker 同步改成表驱动,顺带认第三种 prototype
- 提示词:prototype.md 要求画完挂 marker;brainstorm-continue.md 新增
  {prototypeRef} 占位符,有原型插件时让续聊先读它别从零重画,没有时
  连同整段抹掉

Claude-Session: https://claude.ai/code/session_01Kgmb1LvrPmFX3NoUDB4zZC
This commit is contained in:
cruldra committed 2026-09-15 00:11:49 +08:00
1 parent def5f4e88c
commit 142af57d9d
24 files changed
+628 -85

No files matched your search

+2
View File
@@ -49,6 +49,7 @@ export function App() {
resumePrototypeSession,
focusSession,
openFile,
openPrototypeInFigma,
implement,
generatePrDiffSummary,
isPrDiffSummaryRunning,
@@ -386,6 +387,7 @@ export function App() {
onStartPrototypeSession={startPrototypeSession}
onResumePrototypeSession={resumePrototypeSession}
onOpenFile={openFile}
onOpenPrototypeInFigma={openPrototypeInFigma}
onImplement={implement}
onOpenPr={openPr}
onGeneratePrDiffSummary={generatePrDiffSummary}
@@ -37,6 +37,7 @@ interface BottomTabsProps {
onStartPrototypeSession: (issueNumber: number) => void
onResumePrototypeSession: (sessionId: string, issueNumber: number, cwd?: string) => void
onOpenFile: (path: string) => void
onOpenPrototypeInFigma: (issueNumber: number, path: string) => void
onImplement: (issueNumber: number, planFile: string, profilePath?: string, sessionId?: string) => void
onOpenPr: (pr: string) => void
onGeneratePrDiffSummary: (issueNumber: number) => void
@@ -140,6 +141,7 @@ export function BottomTabs(props: BottomTabsProps) {
onStartPrototypeSession={props.onStartPrototypeSession}
onResumePrototypeSession={props.onResumePrototypeSession}
onOpenFile={props.onOpenFile}
onOpenPrototypeInFigma={props.onOpenPrototypeInFigma}
onImplement={props.onImplement}
onOpenPr={props.onOpenPr}
onGeneratePrDiffSummary={props.onGeneratePrDiffSummary}
@@ -69,6 +69,11 @@ interface IssueDetailPanelProps {
onStartPrototypeSession: (issueNumber: number) => void
/** Open a workspace-relative file in the editor. */
onOpenFile: (path: string) => void
/**
* 把工单的 Figma 插件 manifest 导进本机 Figma 桌面版并拉起 Figma。
* 「原型插件路径」行尾那个按钮。
*/
onOpenPrototypeInFigma: (issueNumber: number, path: string) => void
/** Kick off the end-to-end implementation flow for the given plan file. */
onImplement: (issueNumber: number, planFile: string, profilePath?: string, sessionId?: string) => void
/** Open the gitea PR page in the browser. */
@@ -151,6 +156,7 @@ export function IssueDetailPanel({
onStartTestSession,
onStartPrototypeSession,
onOpenFile,
onOpenPrototypeInFigma,
onImplement,
onOpenPr,
onGeneratePrDiffSummary,
@@ -567,6 +573,24 @@ export function IssueDetailPanel({
}]
: [],
},
{
key: 'prototypeFile',
label: '原型插件路径',
type: 'file-link',
description: '双击在编辑器打开 Figma 插件的 manifest.json;点右侧按钮把插件导入本机 Figma 并打开;cc 通过 issue body 的 <!-- spx:prototype=路径 --> 注释同步',
onOpen: (p: string) => onOpenFile(p),
// 没挂原型插件时整个按钮不渲染,行只显示空态。
secondaryActions: issue?.prototypeFile
? [{
icon: <ExternalLink className="size-3.5" />,
title: '在 Figma 中导入并打开',
onClick: () => {
if (issue?.prototypeFile)
onOpenPrototypeInFigma(issue.number, issue.prototypeFile)
},
}]
: [],
},
{
key: 'pr',
label: '合并请求',
@@ -641,7 +665,7 @@ export function IssueDetailPanel({
],
},
],
[onResumeSession, onResumeReviewSession, onResumeTestSession, onStartTestSession, onResumePrototypeSession, onStartPrototypeSession, onOpenFile, onImplement, onOpenPr, onGeneratePrDiffSummary, onOpenWorktree, onDeleteWorktree, onMergeBranch, onCloseSessionTab, onContinueBrainstorm, onClearBrainstormSession, onOpenLogs, issue, locked, lockTitle, globalAutoReview, prDiffSummaryRunning, profiles, isSessionTabPending, onStartReviewSession],
[onResumeSession, onResumeReviewSession, onResumeTestSession, onStartTestSession, onResumePrototypeSession, onStartPrototypeSession, onOpenFile, onOpenPrototypeInFigma, onImplement, onOpenPr, onGeneratePrDiffSummary, onOpenWorktree, onDeleteWorktree, onMergeBranch, onCloseSessionTab, onContinueBrainstorm, onClearBrainstormSession, onOpenLogs, issue, locked, lockTitle, globalAutoReview, prDiffSummaryRunning, profiles, isSessionTabPending, onStartReviewSession],
)
// 同事的工单只读:属性表统一消毒,顶部只留查看类按钮。
@@ -671,6 +695,7 @@ export function IssueDetailPanel({
prototypeProfilePath: matchProfileOption(issue.prototypeProfilePath, profiles) || null,
specFile: issue.specFile ?? null,
planFile: issue.planFile ?? null,
prototypeFile: issue.prototypeFile ?? null,
prDiffFile: issue.prDiffFile ?? null,
pr: issue.pr
? `${issue.pr}${issue.prMerged ? '(已合并)' : ''}`
@@ -994,10 +994,11 @@ export function SettingsModal({
label="头脑风暴续聊提示词"
hint={(
<>
可用占位符
{' '}
可用占位符:
<code>{'{issueNumber}'}</code>
;用于对已有工单继续讨论 / 写 spec/plan 的会话初始指令。
、
<code>{'{prototypeRef}'}</code>
(工单挂了原型插件才填,没挂时该段整段删掉);用于对已有工单继续讨论 / 写 spec/plan 的会话初始指令。
</>
)}
>
+7 -1
View File
@@ -200,6 +200,8 @@ export interface UseIssuesResult {
resumePrototypeSession: (sessionId: string, issueNumber: number, cwd?: string) => void
focusSession: (issueNumber: number) => void
openFile: (path: string) => void
/** 把工单的 Figma 插件导入本机 Figma 桌面版并拉起 Figma。 */
openPrototypeInFigma: (issueNumber: number, path: string) => void
implement: (issueNumber: number, planFile: string, profilePath?: string, sessionId?: string) => void
generatePrDiffSummary: (issueNumber: number) => void
isPrDiffSummaryRunning: (issueNumber: number) => boolean
@@ -588,6 +590,10 @@ export function useIssues(): UseIssuesResult {
postMessage({ type: 'editor/open-file', path })
}, [])
const openPrototypeInFigma = useCallback((issueNumber: number, path: string): void => {
postMessage({ type: 'prototype/open-in-figma', issueNumber, path })
}, [])
const implement = useCallback((
issueNumber: number,
planFile: string,
@@ -1037,5 +1043,5 @@ export function useIssues(): UseIssuesResult {
return cleanup
}, [clearPrDiffSummaryRunning, markHandoffRunning])
return { state, settings, avatars, isSessionTabPending, globalAutoReview, youtrackConfigured, importYouTrack, toasts, profiles, setIssues, refresh, refreshing, scope, toggleScope, saveSettings, listYouTrackProjects, youtrackProjects, dismissSettings, requestEditAuth, createIssue, dismissToast, openUrl, resumeSession, resumeReviewSession, startReviewSession, startTestSession, resumeTestSession, startPrototypeSession, resumePrototypeSession, focusSession, openFile, implement, generatePrDiffSummary, isPrDiffSummaryRunning, openPr, openWorktree, deleteWorktree, mergeBranch, deleteIssue, closeIssue, closeSessionTab, continueBrainstorm, changeColumn, setDependency, clearDependency, updateIssueAutoReview, updateIssueProfilePath, updateIssueBrainstormProfilePath, clearBrainstormSession, updateIssueTestProfilePath, updateIssuePrototypeProfilePath, logs, fetchLogs, clearLogs, pendingSelectId, clearPendingSelect, commitRunning, runCommit, hasChanges, release, runRelease, cherryPick, dismissCherryPick, confirmCherryPick, envLocked, envFileCount, envLockRunning, envLockTitle, toggleEnvLock, me, handoffUsers, requestHandoffUsers, startHandoff, acceptHandoff, isHandoffRunning }
return { state, settings, avatars, isSessionTabPending, globalAutoReview, youtrackConfigured, importYouTrack, toasts, profiles, setIssues, refresh, refreshing, scope, toggleScope, saveSettings, listYouTrackProjects, youtrackProjects, dismissSettings, requestEditAuth, createIssue, dismissToast, openUrl, resumeSession, resumeReviewSession, startReviewSession, startTestSession, resumeTestSession, startPrototypeSession, resumePrototypeSession, focusSession, openFile, openPrototypeInFigma, implement, generatePrDiffSummary, isPrDiffSummaryRunning, openPr, openWorktree, deleteWorktree, mergeBranch, deleteIssue, closeIssue, closeSessionTab, continueBrainstorm, changeColumn, setDependency, clearDependency, updateIssueAutoReview, updateIssueProfilePath, updateIssueBrainstormProfilePath, clearBrainstormSession, updateIssueTestProfilePath, updateIssuePrototypeProfilePath, logs, fetchLogs, clearLogs, pendingSelectId, clearPendingSelect, commitRunning, runCommit, hasChanges, release, runRelease, cherryPick, dismissCherryPick, confirmCherryPick, envLocked, envFileCount, envLockRunning, envLockTitle, toggleEnvLock, me, handoffUsers, requestHandoffUsers, startHandoff, acceptHandoff, isHandoffRunning }
}
+2 -1
View File
@@ -83,7 +83,7 @@ export type ExtensionToWebview
| { type: 'issues/loaded' }
| { type: 'issues/update', issues: Issue[], scope: 'mine' | 'all', globalAutoReview: boolean, youtrackConfigured: boolean, me?: string, avatars?: Record<string, string> }
| { type: 'issues/error', message: string }
| { type: 'issue/patch', issueNumber: number, patch: { autoReview?: boolean, specFile?: string, planFile?: string, prDiffFile?: string | null, sessionId?: string | null, implementSessionId?: string | null, reviewSessionId?: string | null, reviewSessionFileExists?: boolean, testSessionId?: string | null, prototypeSessionId?: string | null, pr?: string | null, implementStatus?: 'running' | 'done' | 'failed' | null, column?: IssueColumn, worktreePath?: string | null, prMerged?: boolean, prMergedAt?: string | null, branch?: string | null, color?: string, worktreeExists?: boolean, brainstormTabOpen?: boolean, implementTabOpen?: boolean, reviewTabOpen?: boolean, testTabOpen?: boolean, prototypeTabOpen?: boolean, profilePath?: string | null, brainstormProfilePath?: string | null, testProfilePath?: string | null, prototypeProfilePath?: string | null, assignees?: string[], handoffAttachmentId?: string | null, handoffFrom?: string | null } }
| { type: 'issue/patch', issueNumber: number, patch: { autoReview?: boolean, specFile?: string, planFile?: string, prototypeFile?: string, prDiffFile?: string | null, sessionId?: string | null, implementSessionId?: string | null, reviewSessionId?: string | null, reviewSessionFileExists?: boolean, testSessionId?: string | null, prototypeSessionId?: string | null, pr?: string | null, implementStatus?: 'running' | 'done' | 'failed' | null, column?: IssueColumn, worktreePath?: string | null, prMerged?: boolean, prMergedAt?: string | null, branch?: string | null, color?: string, worktreeExists?: boolean, brainstormTabOpen?: boolean, implementTabOpen?: boolean, reviewTabOpen?: boolean, testTabOpen?: boolean, prototypeTabOpen?: boolean, profilePath?: string | null, brainstormProfilePath?: string | null, testProfilePath?: string | null, prototypeProfilePath?: string | null, assignees?: string[], handoffAttachmentId?: string | null, handoffFrom?: string | null } }
/** 某个会话 id 的开 tab 请求已处理完(开成、复用或被拒都算),webview 据此停掉行内 spinner。 */
| { type: 'session/tab-settled', sessionId: string }
| { type: 'issue/pr-diff-summary-done', issueNumber: number }
@@ -234,6 +234,7 @@ export type WebviewToExtension
| { type: 'session/start-review', issueNumber: number }
| { type: 'session/start-test', issueNumber: number }
| { type: 'session/start-prototype', issueNumber: number }
| { type: 'prototype/open-in-figma', issueNumber: number, path: string }
| { type: 'editor/open-file', path: string }
| { type: 'profiles/list' }
| { type: 'issue/implement', issueNumber: number, planFile: string, profilePath?: string, sessionId?: string }
+5
View File
@@ -40,6 +40,11 @@ export interface Issue {
* `.claude/plans/<stem>.md`.
*/
planFile?: string
/**
* 原型会话产出的 Figma 插件 `manifest.json` 路径(仓库相对或绝对)。
* cc 画完原型后用 `spx issue marker --type prototype` 挂上来。
*/
prototypeFile?: string
/** Optional workspace-relative path to the generated PR diff summary. */
prDiffFile?: string
/** PR number associated with this issue (set after webhook fires). */