✨ 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

@@ -51,12 +51,22 @@ interface IssueDetailPanelProps {
* conversation associated with this issue.
*/
onResumeTestSession: (sessionId: string, issueNumber: number, cwd?: string) => void
/**
* Open a new terminal that runs `claude --resume <id>` for the prototype
* conversation associated with this issue.
*/
onResumePrototypeSession: (sessionId: string, issueNumber: number, cwd?: string) => void
/**
* Spawn a fresh "测试" cc tab for an issue whose PR has merged. Bound to the
* Play button next to the "测试会话 id" row (only shown when `issue.pr` exists).
*/
onStartReviewSession: (issueNumber: number) => void
onStartTestSession: (issueNumber: number) => void
/**
* Spawn a fresh "原型" cc tab. Bound to the Play button next to the
* "原型会话 id" row;不依赖 PR,任何阶段都能起。
*/
onStartPrototypeSession: (issueNumber: number) => void
/** Open a workspace-relative file in the editor. */
onOpenFile: (path: string) => void
/** Kick off the end-to-end implementation flow for the given plan file. */
@@ -90,7 +100,7 @@ interface IssueDetailPanelProps {
* onDidCloseTerminal and clears the corresponding `*TabOpen` flag, which
* makes the X button vanish on its own.
*/
onCloseSessionTab: (issueNumber: number, kind: 'brainstorm' | 'implement' | 'review' | 'test') => void
onCloseSessionTab: (issueNumber: number, kind: 'brainstorm' | 'implement' | 'review' | 'test' | 'prototype') => void
/**
* 「▶ 续聊」:由插件把续聊提示词送进头脑风暴会话——规划 tab 开着就直接
* 打进去,关了就 `claude --resume <id>` 带着提示词恢复,没会话就新开一个。
@@ -107,6 +117,8 @@ interface IssueDetailPanelProps {
onUpdateBrainstormProfilePath: (issueNumber: number, brainstormProfilePath: string) => void
/** Persist a per-issue `testProfilePath` override into the state JSON. */
onUpdateTestProfilePath: (issueNumber: number, testProfilePath: string) => void
/** Persist a per-issue `prototypeProfilePath` override into the state JSON. */
onUpdatePrototypeProfilePath: (issueNumber: number, prototypeProfilePath: string) => void
/** Open the in-webview log modal. */
onOpenLogs: () => void
/** 当前 Gitea login;undefined = 身份未知(接管按钮不显示)。 */
@@ -134,8 +146,10 @@ export function IssueDetailPanel({
onResumeSession,
onResumeReviewSession,
onResumeTestSession,
onResumePrototypeSession,
onStartReviewSession,
onStartTestSession,
onStartPrototypeSession,
onOpenFile,
onImplement,
onOpenPr,
@@ -155,6 +169,7 @@ export function IssueDetailPanel({
onUpdateProfilePath,
onUpdateBrainstormProfilePath,
onUpdateTestProfilePath,
onUpdatePrototypeProfilePath,
onOpenLogs,
me,
isSessionTabPending,
@@ -402,6 +417,51 @@ export function IssueDetailPanel({
secondaryActions,
}
})(),
(() => {
// 原型会话 id 行:与测试行同构的三态,差别是启动不看 PR——原型只照
// 工单正文画界面,任何阶段都能起;前置工单没做完时按钮禁用。
const hasSession = !!(issue?.prototypeSessionId && issue.prototypeSessionId.length > 0)
const secondaryActions: RowAction[] = []
if (hasSession && issue?.prototypeTabOpen) {
secondaryActions.push({
icon: <X className="size-3.5" />,
title: '关闭原型 tab',
onClick: () => {
if (issue)
onCloseSessionTab(issue.number, 'prototype')
},
})
}
else if (!hasSession) {
secondaryActions.push({
icon: <Play className="size-3.5" />,
title: locked ? lockTitle : '启动原型会话',
disabled: locked,
onClick: () => {
if (issue)
onStartPrototypeSession(issue.number)
},
})
}
return {
key: 'prototypeSessionId',
label: '原型会话id',
type: 'action' as const,
pending: hasSession ? isSessionTabPending(issue!.prototypeSessionId!) : false,
description: hasSession
? '双击在新终端运行 claude --resume <id> 恢复原型对话(worktree 在则用之,否则工作区根目录)'
: '点击右侧 ▶ 启动一个原型 cc tab:照工单正文画界面原型,不依赖 PR',
actionIcon: <Terminal className="size-3.5" />,
onAction: (v: unknown) => {
if (typeof v !== 'string' || v.length === 0 || !issue)
return
if (!throttleBySessionId(v))
return
onResumePrototypeSession(v, issue.number)
},
secondaryActions,
}
})(),
{
key: 'autoReview',
label: '自动审查',
@@ -458,6 +518,19 @@ export function IssueDetailPanel({
})(),
description: '测试会话使用的 Claude 配置文件;选「(默认)」用内置默认,与头脑风暴/实施互不回退',
},
{
key: 'prototypeProfilePath',
label: '原型配置文件',
type: 'select',
options: (() => {
const opts = [{ label: '(默认)', value: '' }, ...profiles.map(p => ({ label: p.name, value: p.path }))]
const current = matchProfileOption(issue?.prototypeProfilePath, profiles)
if (current && !opts.some(o => o.value === current))
opts.push({ label: `自定义:${current}`, value: current })
return opts
})(),
description: '原型会话使用的 Claude 配置文件;选「(默认)」用内置默认,与其它会话互不回退',
},
{
key: 'specFile',
label: '规格文件',
@@ -568,7 +641,7 @@ export function IssueDetailPanel({
],
},
],
[onResumeSession, onResumeReviewSession, onResumeTestSession, onStartTestSession, 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, onImplement, onOpenPr, onGeneratePrDiffSummary, onOpenWorktree, onDeleteWorktree, onMergeBranch, onCloseSessionTab, onContinueBrainstorm, onClearBrainstormSession, onOpenLogs, issue, locked, lockTitle, globalAutoReview, prDiffSummaryRunning, profiles, isSessionTabPending, onStartReviewSession],
)
// 同事的工单只读:属性表统一消毒,顶部只留查看类按钮。
@@ -588,12 +661,14 @@ export function IssueDetailPanel({
implementSessionId: issue.implementSessionId ?? null,
reviewSessionId: issue.reviewSessionId ?? null,
testSessionId: issue.testSessionId ?? null,
prototypeSessionId: issue.prototypeSessionId ?? null,
autoReview: issue.autoReview ?? globalAutoReview,
color: issue.color ?? null,
// 展示/选中值按 basename 映射到本机 profiles,避免跨机绝对路径变成「自定义」项。
brainstormProfilePath: matchProfileOption(issue.brainstormProfilePath, profiles) || null,
profilePath: matchProfileOption(issue.profilePath, profiles) || null,
testProfilePath: matchProfileOption(issue.testProfilePath, profiles) || null,
prototypeProfilePath: matchProfileOption(issue.prototypeProfilePath, profiles) || null,
specFile: issue.specFile ?? null,
planFile: issue.planFile ?? null,
prDiffFile: issue.prDiffFile ?? null,
@@ -788,6 +863,8 @@ export function IssueDetailPanel({
onUpdateProfilePath(issue.number, value)
if (key === 'testProfilePath' && issue && typeof value === 'string')
onUpdateTestProfilePath(issue.number, value)
if (key === 'prototypeProfilePath' && issue && typeof value === 'string')
onUpdatePrototypeProfilePath(issue.number, value)
}}
/>
</div>