✨ 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:
1 parent
3fff796a61
commit
b62e58d739
27 files changed
+760
-50
No files matched your search
@@ -34,6 +34,8 @@ interface BottomTabsProps {
|
||||
onStartReviewSession: (issueNumber: number) => void
|
||||
onStartTestSession: (issueNumber: number) => void
|
||||
onResumeTestSession: (sessionId: string, issueNumber: number, cwd?: string) => void
|
||||
onStartPrototypeSession: (issueNumber: number) => void
|
||||
onResumePrototypeSession: (sessionId: string, issueNumber: number, cwd?: string) => void
|
||||
onOpenFile: (path: string) => void
|
||||
onImplement: (issueNumber: number, planFile: string, profilePath?: string, sessionId?: string) => void
|
||||
onOpenPr: (pr: string) => void
|
||||
@@ -45,13 +47,14 @@ interface BottomTabsProps {
|
||||
onDeleteIssue?: (issueNumber: number) => void
|
||||
onCloseIssue: (issueNumber: number) => void
|
||||
onResetToTodo: (issueNumber: number) => void
|
||||
onCloseSessionTab: (issueNumber: number, kind: 'brainstorm' | 'implement' | 'review' | 'test') => void
|
||||
onCloseSessionTab: (issueNumber: number, kind: 'brainstorm' | 'implement' | 'review' | 'test' | 'prototype') => void
|
||||
onContinueBrainstorm: (issueNumber: number) => void
|
||||
onClearBrainstormSession: (issueNumber: number) => void
|
||||
onUpdateAutoReview: (issueNumber: number, value: boolean) => void
|
||||
onUpdateProfilePath: (issueNumber: number, profilePath: string) => void
|
||||
onUpdateBrainstormProfilePath: (issueNumber: number, brainstormProfilePath: string) => void
|
||||
onUpdateTestProfilePath: (issueNumber: number, testProfilePath: string) => void
|
||||
onUpdatePrototypeProfilePath: (issueNumber: number, prototypeProfilePath: string) => void
|
||||
onOpenLogs: () => void
|
||||
me?: string
|
||||
/** Gitea login → 头像 data URI。 */
|
||||
@@ -134,6 +137,8 @@ export function BottomTabs(props: BottomTabsProps) {
|
||||
onStartReviewSession={props.onStartReviewSession}
|
||||
onStartTestSession={props.onStartTestSession}
|
||||
onResumeTestSession={props.onResumeTestSession}
|
||||
onStartPrototypeSession={props.onStartPrototypeSession}
|
||||
onResumePrototypeSession={props.onResumePrototypeSession}
|
||||
onOpenFile={props.onOpenFile}
|
||||
onImplement={props.onImplement}
|
||||
onOpenPr={props.onOpenPr}
|
||||
@@ -152,6 +157,7 @@ export function BottomTabs(props: BottomTabsProps) {
|
||||
onUpdateProfilePath={props.onUpdateProfilePath}
|
||||
onUpdateBrainstormProfilePath={props.onUpdateBrainstormProfilePath}
|
||||
onUpdateTestProfilePath={props.onUpdateTestProfilePath}
|
||||
onUpdatePrototypeProfilePath={props.onUpdatePrototypeProfilePath}
|
||||
profiles={props.profiles}
|
||||
onOpenLogs={props.onOpenLogs}
|
||||
me={props.me}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -26,6 +26,7 @@ interface SubmitValues {
|
||||
autoReview: boolean
|
||||
reviewPrompt: string
|
||||
testPrompt: string
|
||||
prototypePrompt: string
|
||||
devBranch: string
|
||||
releaseBranchPrefix: string
|
||||
jenkinsJob: string
|
||||
@@ -41,6 +42,7 @@ interface SubmitValues {
|
||||
defaultBrainstormProfilePath: string
|
||||
defaultImplementProfilePath: string
|
||||
defaultTestProfilePath: string
|
||||
defaultPrototypeProfilePath: string
|
||||
conflictResolutionProfilePath: string
|
||||
commitProfilePath: string
|
||||
prDiffSummaryProfilePath: string
|
||||
@@ -80,6 +82,7 @@ export interface SettingsModalProps {
|
||||
initialAutoReview: boolean
|
||||
initialReviewPrompt: string
|
||||
initialTestPrompt: string
|
||||
initialPrototypePrompt: string
|
||||
initialDevBranch: string
|
||||
initialReleaseBranchPrefix: string
|
||||
initialJenkinsJob: string
|
||||
@@ -95,6 +98,7 @@ export interface SettingsModalProps {
|
||||
initialDefaultBrainstormProfilePath: string
|
||||
initialDefaultImplementProfilePath: string
|
||||
initialDefaultTestProfilePath: string
|
||||
initialDefaultPrototypeProfilePath: string
|
||||
initialConflictResolutionProfilePath: string
|
||||
initialCommitProfilePath: string
|
||||
initialPrDiffSummaryProfilePath: string
|
||||
@@ -170,6 +174,7 @@ export function SettingsModal({
|
||||
initialAutoReview,
|
||||
initialReviewPrompt,
|
||||
initialTestPrompt,
|
||||
initialPrototypePrompt,
|
||||
initialDevBranch,
|
||||
initialReleaseBranchPrefix,
|
||||
initialJenkinsJob,
|
||||
@@ -185,6 +190,7 @@ export function SettingsModal({
|
||||
initialDefaultBrainstormProfilePath,
|
||||
initialDefaultImplementProfilePath,
|
||||
initialDefaultTestProfilePath,
|
||||
initialDefaultPrototypeProfilePath,
|
||||
initialConflictResolutionProfilePath,
|
||||
initialCommitProfilePath,
|
||||
initialPrDiffSummaryProfilePath,
|
||||
@@ -212,6 +218,7 @@ export function SettingsModal({
|
||||
const [autoReview, setAutoReview] = useState(initialAutoReview)
|
||||
const [reviewPrompt, setReviewPrompt] = useState(initialReviewPrompt)
|
||||
const [testPrompt, setTestPrompt] = useState(initialTestPrompt)
|
||||
const [prototypePrompt, setPrototypePrompt] = useState(initialPrototypePrompt)
|
||||
const [devBranch, setDevBranch] = useState(initialDevBranch)
|
||||
const [releaseBranchPrefix, setReleaseBranchPrefix] = useState(initialReleaseBranchPrefix)
|
||||
const [jenkinsJob, setJenkinsJob] = useState(initialJenkinsJob)
|
||||
@@ -227,6 +234,7 @@ export function SettingsModal({
|
||||
const [defaultBrainstormProfilePath, setDefaultBrainstormProfilePath] = useState(initialDefaultBrainstormProfilePath)
|
||||
const [defaultImplementProfilePath, setDefaultImplementProfilePath] = useState(initialDefaultImplementProfilePath)
|
||||
const [defaultTestProfilePath, setDefaultTestProfilePath] = useState(initialDefaultTestProfilePath)
|
||||
const [defaultPrototypeProfilePath, setDefaultPrototypeProfilePath] = useState(initialDefaultPrototypeProfilePath)
|
||||
const [conflictResolutionProfilePath, setConflictResolutionProfilePath] = useState(initialConflictResolutionProfilePath)
|
||||
const [commitProfilePath, setCommitProfilePath] = useState(initialCommitProfilePath)
|
||||
const [prDiffSummaryProfilePath, setPrDiffSummaryProfilePath] = useState(initialPrDiffSummaryProfilePath)
|
||||
@@ -256,6 +264,7 @@ export function SettingsModal({
|
||||
setAutoReview(initialAutoReview)
|
||||
setReviewPrompt(initialReviewPrompt)
|
||||
setTestPrompt(initialTestPrompt)
|
||||
setPrototypePrompt(initialPrototypePrompt)
|
||||
setDevBranch(initialDevBranch)
|
||||
setReleaseBranchPrefix(initialReleaseBranchPrefix)
|
||||
setJenkinsJob(initialJenkinsJob)
|
||||
@@ -271,6 +280,7 @@ export function SettingsModal({
|
||||
setDefaultBrainstormProfilePath(initialDefaultBrainstormProfilePath)
|
||||
setDefaultImplementProfilePath(initialDefaultImplementProfilePath)
|
||||
setDefaultTestProfilePath(initialDefaultTestProfilePath)
|
||||
setDefaultPrototypeProfilePath(initialDefaultPrototypeProfilePath)
|
||||
setConflictResolutionProfilePath(initialConflictResolutionProfilePath)
|
||||
setCommitProfilePath(initialCommitProfilePath)
|
||||
setPrDiffSummaryProfilePath(initialPrDiffSummaryProfilePath)
|
||||
@@ -348,6 +358,7 @@ export function SettingsModal({
|
||||
autoReview,
|
||||
reviewPrompt,
|
||||
testPrompt,
|
||||
prototypePrompt,
|
||||
devBranch: devBranch.trim(),
|
||||
releaseBranchPrefix: releaseBranchPrefix.trim(),
|
||||
jenkinsJob: jenkinsJob.trim(),
|
||||
@@ -363,10 +374,11 @@ export function SettingsModal({
|
||||
implTabPreCreateScript: implTabPreCreateScript.trim(),
|
||||
implTabPostCloseScript: implTabPostCloseScript.trim(),
|
||||
splitNewTerminals,
|
||||
// 三个会话默认 profile:空串 = 落到 DEFAULT_PROFILE_PATH
|
||||
// 四个会话默认 profile:空串 = 落到 DEFAULT_PROFILE_PATH
|
||||
defaultBrainstormProfilePath: defaultBrainstormProfilePath.trim(),
|
||||
defaultImplementProfilePath: defaultImplementProfilePath.trim(),
|
||||
defaultTestProfilePath: defaultTestProfilePath.trim(),
|
||||
defaultPrototypeProfilePath: defaultPrototypeProfilePath.trim(),
|
||||
// 冲突解决 profile:空串 = 用 DEFAULT_PROFILE_PATH
|
||||
conflictResolutionProfilePath: conflictResolutionProfilePath.trim(),
|
||||
// 提交代码 profile:空串 = 未配置 → 提交严格失败
|
||||
@@ -625,6 +637,30 @@ export function SettingsModal({
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
label="原型默认配置文件"
|
||||
hint={(
|
||||
<>
|
||||
原型会话默认用此 profile;工单详情面板里的「原型配置文件」优先级更高。留空用 official。
|
||||
</>
|
||||
)}
|
||||
>
|
||||
<SelectMenu
|
||||
value={defaultPrototypeProfilePath}
|
||||
onChange={setDefaultPrototypeProfilePath}
|
||||
variant="input"
|
||||
className={inputClass}
|
||||
options={[
|
||||
{ value: '', label: '(默认)' },
|
||||
...profiles.map(p => ({ value: p.path, label: p.name })),
|
||||
...(defaultPrototypeProfilePath
|
||||
&& !profiles.some(p => p.path === defaultPrototypeProfilePath)
|
||||
? [{ value: defaultPrototypeProfilePath, label: `自定义:${defaultPrototypeProfilePath}` }]
|
||||
: []),
|
||||
]}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
label="冲突解决配置文件"
|
||||
hint={(
|
||||
@@ -1028,6 +1064,23 @@ export function SettingsModal({
|
||||
className={`${inputClass} resize-y font-mono`}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
label="原型提示词"
|
||||
hint={(
|
||||
<>
|
||||
可用占位符:
|
||||
<code>{'{issueNumber}'}</code>
|
||||
</>
|
||||
)}
|
||||
>
|
||||
<textarea
|
||||
rows={4}
|
||||
value={prototypePrompt}
|
||||
onChange={e => setPrototypePrompt(e.target.value)}
|
||||
className={`${inputClass} resize-y font-mono`}
|
||||
/>
|
||||
</Field>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
Reference in new issue
Block a user