✨ feat(vscode): 新增第五种工单会话「原型会话」

照测试会话那条链路补一份 prototype:

- 消息与路由:session/start-prototype、session/resume-prototype、
  issue/update-prototype-profile-path,都进只读门禁
- 会话:handleStartPrototypeSession / handleResumePrototypeSession,
  终端名 issue-N-原型;cwd 有 worktree 用 worktree、没有退主仓库根
  (原 resolveTestSessionCwd 改名 resolveWorktreeOrRootCwd 两种会话共用)
- 不要求有 PR,任何阶段都能起;前置工单锁仍然拦
- 提示词:新增 prompts/prototype.md,设置里可改,占位符 {issueNumber}
- profile:工单级 prototypeProfilePath > 设置里的原型默认 profile >
  内置默认,与其它会话互不回退
- 状态字段 prototypeSessionId / prototypeProfilePath / prototypeTabOpen
  贯通 gitea、youtrack、本机 state 与移交包
- 详情面板加「原型会话id」「原型配置文件」两行,设置面板加对应两项

Claude-Session: https://claude.ai/code/session_01Kgmb1LvrPmFX3NoUDB4zZC
This commit is contained in:
cruldra committed 2026-09-14 10:20:30 +08:00
1 parent 3fff796a61
commit b62e58d739
27 files changed
+760 -50

No files matched your search

+95 -3
View File
@@ -117,6 +117,7 @@ export async function handleColumnChange(panel: KanbanWebviewPanel, issueNumber:
let fromColumn: IssueColumn | undefined
let implementSessionId: string | undefined
let testSessionId: string | undefined
let prototypeSessionId: string | undefined
let featureBranchFromState: string | undefined
try {
const stateObj = await panel.readIssueState(issueNumber)
@@ -134,6 +135,8 @@ export async function handleColumnChange(panel: KanbanWebviewPanel, issueNumber:
implementSessionId = stateObj.implementSessionId
if (typeof stateObj.testSessionId === 'string' && stateObj.testSessionId.length > 0)
testSessionId = stateObj.testSessionId
if (typeof stateObj.prototypeSessionId === 'string' && stateObj.prototypeSessionId.length > 0)
prototypeSessionId = stateObj.prototypeSessionId
if (typeof stateObj.branch === 'string' && stateObj.branch.length > 0)
featureBranchFromState = stateObj.branch
}
@@ -394,13 +397,13 @@ export async function handleColumnChange(panel: KanbanWebviewPanel, issueNumber:
issueNumber,
})
// 删 worktree 前把实施/测试会话整包(jsonl + 子目录)搬到主仓库 projects 目录,
// 删 worktree 前把实施/测试/原型会话整包(jsonl + 子目录)搬到主仓库 projects 目录,
// 并清掉 worktree 侧副本:新版 claude 遇到重复 sid 会拒绝 resume。
if ((implementSessionId || testSessionId) && worktreePath) {
if ((implementSessionId || testSessionId || prototypeSessionId) && worktreePath) {
const worktreeAbs = resolveWorktreePath(worktreePath, workspaceRoot)
const srcProjectsDir = projectsDirFor(worktreeAbs)
const dstProjectsDir = projectsDirFor(workspaceRoot)
for (const sid of [implementSessionId, testSessionId]) {
for (const sid of [implementSessionId, testSessionId, prototypeSessionId]) {
if (typeof sid !== 'string' || sid.length === 0)
continue
try {
@@ -2210,6 +2213,95 @@ export async function handleUpdateTestProfilePath(panel: KanbanWebviewPanel, iss
}
}
/**
* Persist a per-issue `prototypeProfilePath` override into the issue's state JSON
* comment. 原型 start/resume 用它(见 resolvePrototypeProfilePath);
* 空 = 设置里的「原型默认配置文件」,再空 = getDefaultProfilePath(),与其它会话互不回退。
*/
export async function handleUpdatePrototypeProfilePath(panel: KanbanWebviewPanel, issueNumber: number, prototypeProfilePath: string): Promise<void> {
const workspaceRoot = workspace.workspaceFolders?.[0]?.uri.fsPath
if (!workspaceRoot) {
panel.postMessage({
type: 'toast/show',
id: makeNonce(),
level: 'error',
message: '请先打开一个工作区文件夹',
dismissOnTimer: 5000,
})
return
}
const remote = await detectRepo(workspaceRoot)
if (!remote) {
panel.postMessage({
type: 'toast/show',
id: makeNonce(),
level: 'error',
message: '当前工作区没有 Gitea 远程仓库',
dismissOnTimer: 5000,
})
return
}
const token = await getToken(panel.context, remote.host)
if (!token) {
panel.postMessage({
type: 'toast/show',
id: makeNonce(),
level: 'error',
message: '请先完成 Gitea 配置',
dismissOnTimer: 5000,
})
return
}
// Snapshot the previous value so we can roll the webview back on failure
// without re-broadcasting the whole issues list.
let previousValue: string | undefined
try {
const existingState = await panel.readIssueState(issueNumber)
previousValue = typeof existingState.prototypeProfilePath === 'string'
? existingState.prototypeProfilePath
: undefined
}
catch {
// If we can't read the previous state, leave previousValue undefined —
// the rollback patch will clear the override, falling back to the default.
previousValue = undefined
}
try {
await panel.mergeIssueState(issueNumber, { prototypeProfilePath })
logger.add({
level: 'info',
source: 'panel',
message: `工单 #${issueNumber} prototypeProfilePath=${prototypeProfilePath} 已持久化`,
})
}
catch (err) {
const message = err instanceof Error ? err.message : String(err)
logger.add({
level: 'error',
source: 'panel',
message: `持久化 prototypeProfilePath 失败 (issue #${issueNumber})`,
details: message,
})
panel.postMessage({
type: 'toast/show',
id: makeNonce(),
level: 'error',
message: `保存工单 #${issueNumber} 原型配置文件失败: ${message}`,
dismissOnTimer: 6000,
})
// Roll back the optimistic update in the webview.
panel.postMessage({
type: 'issue/patch',
issueNumber,
patch: { prototypeProfilePath: previousValue },
})
}
}
export async function handleOpenFile(panel: KanbanWebviewPanel, relPath: string): Promise<void> {
const workspaceRoot = workspace.workspaceFolders?.[0]?.uri.fsPath
if (!workspaceRoot) {