✨ 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

+10
View File
@@ -16,6 +16,7 @@ import * as fs from 'node:fs'
import * as path from 'node:path'
import { env, TabInputTerminal, ThemeColor, Uri, ViewColumn, window, workspace } from 'vscode'
import { deleteToken, getToken } from '../auth/secrets'
import { importPluginIntoFigma } from '../figma/importPlugin'
import { detectRepo } from '../git/remote'
import {
GiteaApiError,
@@ -464,6 +465,15 @@ export class KanbanWebviewPanel {
void issues.handleOpenFile(this, msg.path)
return
}
if (msg.type === 'prototype/open-in-figma') {
const root = workspace.workspaceFolders?.[0]?.uri.fsPath
if (!root) {
void window.showErrorMessage('请先打开一个工作区文件夹')
return
}
void importPluginIntoFigma({ issueNumber: msg.issueNumber, manifestPath: msg.path, workspaceRoot: root })
return
}
if (msg.type === 'issue/implement') {
void this.handleImplement(msg.issueNumber, msg.planFile, msg.profilePath, msg.sessionId)
return
+2 -1
View File
@@ -2314,7 +2314,8 @@ export async function handleOpenFile(panel: KanbanWebviewPanel, relPath: string)
})
return
}
const abs = path.join(workspaceRoot, relPath)
// 多数字段存的是仓库相对路径,但 prototypeFile 允许绝对路径(插件可能在仓库外)。
const abs = path.isAbsolute(relPath) ? relPath : path.join(workspaceRoot, relPath)
const isMarkdown = abs.toLowerCase().endsWith('.md')
try {
if (isMarkdown) {
+9 -1
View File
@@ -947,12 +947,16 @@ export async function handleContinueBrainstorm(panel: KanbanWebviewPanel, issueN
// 头脑风暴只用 brainstormProfilePath;空 = DEFAULT,不回退实施 profile。
let brainstormProfilePath: string | undefined
let sessionId: string | undefined
// 工单已有原型插件时,续聊提示词里让 cc 先读它,别把已经画定的界面又讨论一遍。
let prototypeFile: string | undefined
try {
const stateObj = await panel.readIssueState(issueNumber)
if (stateObj && typeof stateObj.brainstormProfilePath === 'string' && stateObj.brainstormProfilePath.length > 0)
brainstormProfilePath = stateObj.brainstormProfilePath
if (stateObj && typeof stateObj.sessionId === 'string' && stateObj.sessionId.length > 0)
sessionId = stateObj.sessionId
if (stateObj && typeof stateObj.prototypeFile === 'string' && stateObj.prototypeFile.length > 0)
prototypeFile = stateObj.prototypeFile
}
catch (err) {
logger.add({
@@ -963,7 +967,11 @@ export async function handleContinueBrainstorm(panel: KanbanWebviewPanel, issueN
})
}
const prompt = getBrainstormContinuePrompt(panel.context, { issueNumber, issueRef: panel.refFor(issueNumber).externalId })
const prompt = getBrainstormContinuePrompt(panel.context, {
issueNumber,
issueRef: panel.refFor(issueNumber).externalId,
prototypeFile,
})
const terminalName = `issue-${issueNumber}-规划`
// 新建工单流程的 tab 叫 issue-new-{nonce}-规划,只能靠 newIssueTerminals 反查。
const linked = panel.newIssueTerminals.get(issueNumber)
+2 -1
View File
@@ -52,7 +52,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 }
@@ -203,6 +203,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 }