✨ 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
@@ -1,15 +1,17 @@
|
||||
export type ClaudeSessionKind = 'brainstorm' | 'implement' | 'test'
|
||||
export type ClaudeSessionKind = 'brainstorm' | 'implement' | 'test' | 'prototype'
|
||||
|
||||
export const CLAUDE_SESSION_FIELDS = [
|
||||
{ field: 'sessionId', kind: 'brainstorm' },
|
||||
{ field: 'implementSessionId', kind: 'implement' },
|
||||
{ field: 'testSessionId', kind: 'test' },
|
||||
] as const satisfies ReadonlyArray<{ field: 'sessionId' | 'implementSessionId' | 'testSessionId', kind: ClaudeSessionKind }>
|
||||
{ field: 'prototypeSessionId', kind: 'prototype' },
|
||||
] as const satisfies ReadonlyArray<{ field: 'sessionId' | 'implementSessionId' | 'testSessionId' | 'prototypeSessionId', kind: ClaudeSessionKind }>
|
||||
|
||||
export interface HandoffSessions {
|
||||
sessionId?: string
|
||||
implementSessionId?: string
|
||||
testSessionId?: string
|
||||
prototypeSessionId?: string
|
||||
reviewSessionId?: string
|
||||
}
|
||||
|
||||
@@ -17,6 +19,7 @@ export interface HandoffProfiles {
|
||||
profilePath?: string
|
||||
brainstormProfilePath?: string
|
||||
testProfilePath?: string
|
||||
prototypeProfilePath?: string
|
||||
}
|
||||
|
||||
export interface HandoffManifest {
|
||||
@@ -38,7 +41,7 @@ export function handoffAttachmentName(issueNumber: number): string {
|
||||
return `spx-handoff-issue-${issueNumber}.tgz`
|
||||
}
|
||||
|
||||
const KINDS: ReadonlySet<string> = new Set<ClaudeSessionKind>(['brainstorm', 'implement', 'test'])
|
||||
const KINDS: ReadonlySet<string> = new Set<ClaudeSessionKind>(['brainstorm', 'implement', 'test', 'prototype'])
|
||||
|
||||
// Claude / Codex 会话 id 都是 UUID;顺手挡掉路径穿越(如 `../x`)之类的伪造条目。
|
||||
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
||||
@@ -80,13 +83,13 @@ export function parseHandoffManifest(json: string, expectedIssue: number): Hando
|
||||
return { id, relPath }
|
||||
})
|
||||
const sessions: HandoffSessions = {}
|
||||
for (const k of ['sessionId', 'implementSessionId', 'testSessionId', 'reviewSessionId'] as const) {
|
||||
for (const k of ['sessionId', 'implementSessionId', 'testSessionId', 'prototypeSessionId', 'reviewSessionId'] as const) {
|
||||
const v = optString(sessionsRaw[k])
|
||||
if (v)
|
||||
sessions[k] = v
|
||||
}
|
||||
const profiles: HandoffProfiles = {}
|
||||
for (const k of ['profilePath', 'brainstormProfilePath', 'testProfilePath'] as const) {
|
||||
for (const k of ['profilePath', 'brainstormProfilePath', 'testProfilePath', 'prototypeProfilePath'] as const) {
|
||||
const v = optString(profilesRaw[k])
|
||||
if (v)
|
||||
profiles[k] = v
|
||||
|
||||
@@ -59,6 +59,14 @@ export function getTestPrompt(ctx: ExtensionContext, vars: { issueNumber: number
|
||||
.join(vars.prRef)
|
||||
}
|
||||
|
||||
/**
|
||||
* 原型会话首个提示词。只认工单号——原型是照工单正文画界面,与 PR / 分支无关。
|
||||
*/
|
||||
export function getPrototypePrompt(ctx: ExtensionContext, vars: { issueNumber: number, issueRef?: string }): string {
|
||||
const tpl = getSettings(ctx).prototypePrompt
|
||||
return tpl.split('{issueNumber}').join(vars.issueRef ?? String(vars.issueNumber))
|
||||
}
|
||||
|
||||
export function getReviewPrompt(ctx: ExtensionContext, vars: { prNumber: string }): string {
|
||||
const tpl = getSettings(ctx).reviewPrompt
|
||||
return tpl.split('{prNumber}').join(vars.prNumber)
|
||||
|
||||
@@ -70,6 +70,7 @@ function parseColumnFromComments(comments: GiteaComment[]): {
|
||||
profilePath?: string
|
||||
brainstormProfilePath?: string
|
||||
testProfilePath?: string
|
||||
prototypeProfilePath?: string
|
||||
specFile?: string
|
||||
planFile?: string
|
||||
prDiffFile?: string
|
||||
@@ -82,6 +83,7 @@ function parseColumnFromComments(comments: GiteaComment[]): {
|
||||
implementSessionId?: string
|
||||
reviewSessionId?: string
|
||||
testSessionId?: string
|
||||
prototypeSessionId?: string
|
||||
color?: string
|
||||
autoReview?: boolean
|
||||
handoffAttachmentId?: string
|
||||
@@ -116,6 +118,7 @@ function parseColumnFromComments(comments: GiteaComment[]): {
|
||||
profilePath?: unknown
|
||||
brainstormProfilePath?: unknown
|
||||
testProfilePath?: unknown
|
||||
prototypeProfilePath?: unknown
|
||||
specFile?: unknown
|
||||
planFile?: unknown
|
||||
prDiffFile?: unknown
|
||||
@@ -128,6 +131,7 @@ function parseColumnFromComments(comments: GiteaComment[]): {
|
||||
implementSessionId?: unknown
|
||||
reviewSessionId?: unknown
|
||||
testSessionId?: unknown
|
||||
prototypeSessionId?: unknown
|
||||
color?: unknown
|
||||
autoReview?: unknown
|
||||
handoffAttachmentId?: unknown
|
||||
@@ -146,6 +150,9 @@ function parseColumnFromComments(comments: GiteaComment[]): {
|
||||
const testProfilePath = typeof obj.testProfilePath === 'string' && obj.testProfilePath.length > 0
|
||||
? obj.testProfilePath
|
||||
: undefined
|
||||
const prototypeProfilePath = typeof obj.prototypeProfilePath === 'string' && obj.prototypeProfilePath.length > 0
|
||||
? obj.prototypeProfilePath
|
||||
: undefined
|
||||
const specFile = isValidSpxFilePath(obj.specFile) ? obj.specFile : undefined
|
||||
const planFile = isValidSpxFilePath(obj.planFile) ? obj.planFile : undefined
|
||||
const prDiffFile = isValidPrDiffFilePath(obj.prDiffFile) ? obj.prDiffFile : undefined
|
||||
@@ -176,6 +183,9 @@ function parseColumnFromComments(comments: GiteaComment[]): {
|
||||
const testSessionId = typeof obj.testSessionId === 'string' && obj.testSessionId.length > 0
|
||||
? obj.testSessionId
|
||||
: undefined
|
||||
const prototypeSessionId = typeof obj.prototypeSessionId === 'string' && obj.prototypeSessionId.length > 0
|
||||
? obj.prototypeSessionId
|
||||
: undefined
|
||||
const color = typeof obj.color === 'string' && obj.color.length > 0
|
||||
? obj.color
|
||||
: undefined
|
||||
@@ -192,6 +202,7 @@ function parseColumnFromComments(comments: GiteaComment[]): {
|
||||
profilePath,
|
||||
brainstormProfilePath,
|
||||
testProfilePath,
|
||||
prototypeProfilePath,
|
||||
specFile,
|
||||
planFile,
|
||||
prDiffFile,
|
||||
@@ -204,6 +215,7 @@ function parseColumnFromComments(comments: GiteaComment[]): {
|
||||
implementSessionId,
|
||||
reviewSessionId,
|
||||
testSessionId,
|
||||
prototypeSessionId,
|
||||
color,
|
||||
autoReview,
|
||||
handoffAttachmentId,
|
||||
@@ -333,6 +345,7 @@ async function buildIssue(opts: {
|
||||
profilePath,
|
||||
brainstormProfilePath,
|
||||
testProfilePath,
|
||||
prototypeProfilePath,
|
||||
specFile,
|
||||
planFile,
|
||||
prDiffFile,
|
||||
@@ -345,6 +358,7 @@ async function buildIssue(opts: {
|
||||
implementSessionId,
|
||||
reviewSessionId,
|
||||
testSessionId,
|
||||
prototypeSessionId,
|
||||
color,
|
||||
autoReview,
|
||||
handoffAttachmentId,
|
||||
@@ -395,6 +409,7 @@ async function buildIssue(opts: {
|
||||
...(profilePath ? { profilePath } : {}),
|
||||
...(brainstormProfilePath ? { brainstormProfilePath } : {}),
|
||||
...(testProfilePath ? { testProfilePath } : {}),
|
||||
...(prototypeProfilePath ? { prototypeProfilePath } : {}),
|
||||
...(specFile ? { specFile } : {}),
|
||||
...(planFile ? { planFile } : {}),
|
||||
...(prDiffFile ? { prDiffFile } : {}),
|
||||
@@ -408,6 +423,7 @@ async function buildIssue(opts: {
|
||||
...(implementSessionId ? { implementSessionId } : {}),
|
||||
...(reviewSessionId ? { reviewSessionId } : {}),
|
||||
...(testSessionId ? { testSessionId } : {}),
|
||||
...(prototypeSessionId ? { prototypeSessionId } : {}),
|
||||
...(prerequisite !== undefined ? { prerequisite } : {}),
|
||||
...(color ? { color } : {}),
|
||||
...(autoReview !== undefined ? { autoReview } : {}),
|
||||
|
||||
@@ -19,7 +19,7 @@ import { listIssueComments, postIssueComment } from './api'
|
||||
* 普通文本评论、审查评论、Gitea 自动关联评论都不含这些字段,因此从尾往前
|
||||
* 扫描时会被跳过,避免它们插队后下一次 merge 从空状态开始丢历史。
|
||||
*/
|
||||
const KNOWN_STATE_FIELDS = ['column', 'sessionId', 'implementSessionId', 'reviewSessionId', 'testSessionId', 'profilePath', 'brainstormProfilePath', 'testProfilePath', 'specFile', 'planFile', 'prDiffFile', 'pr', 'prMerged', 'prMergedAt', 'branch', 'worktreePath', 'implementStatus', 'color', 'autoReview', 'handoffAttachmentId', 'handoffFrom'] as const
|
||||
const KNOWN_STATE_FIELDS = ['column', 'sessionId', 'implementSessionId', 'reviewSessionId', 'testSessionId', 'prototypeSessionId', 'profilePath', 'brainstormProfilePath', 'testProfilePath', 'prototypeProfilePath', 'specFile', 'planFile', 'prDiffFile', 'pr', 'prMerged', 'prMergedAt', 'branch', 'worktreePath', 'implementStatus', 'color', 'autoReview', 'handoffAttachmentId', 'handoffFrom'] as const
|
||||
|
||||
/**
|
||||
* body 能 parse 成 object 且含至少一个已知 state 字段 → 认为是 state JSON。
|
||||
|
||||
@@ -45,6 +45,8 @@ export interface Issue {
|
||||
brainstormProfilePath?: string
|
||||
/** 测试会话专用的 Claude 配置文件;空 = DEFAULT_PROFILE_PATH,不回退其它字段。 */
|
||||
testProfilePath?: string
|
||||
/** 原型会话专用的 Claude 配置文件;空 = DEFAULT_PROFILE_PATH,不回退其它字段。 */
|
||||
prototypeProfilePath?: string
|
||||
/**
|
||||
* Optional workspace-relative path to the spec file for this issue, as
|
||||
* surfaced from the Claude session transcript. Lives under
|
||||
@@ -100,6 +102,11 @@ export interface Issue {
|
||||
* is merged; reused for resume from the detail panel.
|
||||
*/
|
||||
testSessionId?: string
|
||||
/**
|
||||
* 原型会话(Claude Code session id)。详情面板点原型会话行的 ▶ 启动后写入,
|
||||
* 之后该行改为可 resume。与 PR 无关,任何阶段都能起。
|
||||
*/
|
||||
prototypeSessionId?: string
|
||||
/** Browser URL to the Gitea issue page (escape hatch button in the UI). */
|
||||
htmlUrl: string
|
||||
/** 前置任务 issue number (Gitea dependencies 取第一个) */
|
||||
@@ -132,4 +139,6 @@ export interface Issue {
|
||||
reviewTabOpen?: boolean
|
||||
/** Whether the test terminal tab is currently alive in this VS Code window. */
|
||||
testTabOpen?: boolean
|
||||
/** Whether the prototype terminal tab is currently alive in this VS Code window. */
|
||||
prototypeTabOpen?: boolean
|
||||
}
|
||||
@@ -17,9 +17,11 @@ export const LOCAL_STATE_FIELDS = [
|
||||
'implementSessionId',
|
||||
'reviewSessionId',
|
||||
'testSessionId',
|
||||
'prototypeSessionId',
|
||||
'profilePath',
|
||||
'brainstormProfilePath',
|
||||
'testProfilePath',
|
||||
'prototypeProfilePath',
|
||||
'worktreePath',
|
||||
'prDiffFile',
|
||||
] as const
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
import type { HookContext } from '../git/worktreeHooks'
|
||||
import type { Issue } from '../gitea/types'
|
||||
import type { IssueRef } from '../issues/stateRouter'
|
||||
import type { SessionTabKind } from './handlers/terminals'
|
||||
import type { IssueOwnershipFacts } from './issueOwnership'
|
||||
import type { KanbanScope } from './kanbanScope'
|
||||
import type { ExtensionToWebview, WebviewToExtension } from './messages'
|
||||
@@ -164,7 +165,7 @@ export class KanbanWebviewPanel {
|
||||
* Map 更易于 inspect / debug。条目数顶天就是当前活跃 tab 数,无压力。
|
||||
*/
|
||||
// internal: handler 模块访问
|
||||
readonly terminalOrigin = new Map<Terminal, { issueNumber: number, kind: 'brainstorm' | 'implement' | 'review' | 'test' }>()
|
||||
readonly terminalOrigin = new Map<Terminal, { issueNumber: number, kind: SessionTabKind }>()
|
||||
|
||||
/**
|
||||
* 防 handleResumeSession/handleResumeReviewSession 在 await 钩子/终端创建期间被重入触发,
|
||||
@@ -296,7 +297,7 @@ export class KanbanWebviewPanel {
|
||||
const origin = this.terminalOrigin.get(closed)
|
||||
// 反查 terminalOrigin 给详情面板推 *TabOpen: false,让关闭按钮消失。
|
||||
this.untrackClosedTerminal(closed)
|
||||
if (origin?.kind === 'implement' || origin?.kind === 'test')
|
||||
if (origin?.kind === 'implement' || origin?.kind === 'test' || origin?.kind === 'prototype')
|
||||
void this.dispatchImplTabPostCloseAsync(origin.issueNumber)
|
||||
// 冲突解决会话不进 terminalOrigin,按终端名触发 post-close 钩子。
|
||||
const conflictMatch = closed.name.match(/^issue-(\d+)-冲突解决/)
|
||||
@@ -451,6 +452,14 @@ export class KanbanWebviewPanel {
|
||||
void sessions.handleResumeTestSession(this, msg.sessionId, msg.issueNumber, msg.cwd)
|
||||
return
|
||||
}
|
||||
if (msg.type === 'session/start-prototype') {
|
||||
void sessions.handleStartPrototypeSession(this, msg.issueNumber)
|
||||
return
|
||||
}
|
||||
if (msg.type === 'session/resume-prototype') {
|
||||
void sessions.handleResumePrototypeSession(this, msg.sessionId, msg.issueNumber, msg.cwd)
|
||||
return
|
||||
}
|
||||
if (msg.type === 'editor/open-file') {
|
||||
void issues.handleOpenFile(this, msg.path)
|
||||
return
|
||||
@@ -550,6 +559,10 @@ export class KanbanWebviewPanel {
|
||||
void issues.handleUpdateTestProfilePath(this, msg.issueNumber, msg.testProfilePath)
|
||||
return
|
||||
}
|
||||
if (msg.type === 'issue/update-prototype-profile-path') {
|
||||
void issues.handleUpdatePrototypeProfilePath(this, msg.issueNumber, msg.prototypeProfilePath)
|
||||
return
|
||||
}
|
||||
if (msg.type === 'logs/fetch') {
|
||||
this.postMessage({ type: 'logs/snapshot', entries: logger.snapshot() })
|
||||
return
|
||||
@@ -989,6 +1002,7 @@ export class KanbanWebviewPanel {
|
||||
autoReview: s.autoReview,
|
||||
reviewPrompt: s.reviewPrompt,
|
||||
testPrompt: s.testPrompt,
|
||||
prototypePrompt: s.prototypePrompt,
|
||||
devBranch: s.devBranch,
|
||||
releaseBranchPrefix: s.releaseBranchPrefix,
|
||||
jenkinsJob: s.jenkinsJob,
|
||||
@@ -1004,6 +1018,7 @@ export class KanbanWebviewPanel {
|
||||
defaultBrainstormProfilePath: s.defaultBrainstormProfilePath,
|
||||
defaultImplementProfilePath: s.defaultImplementProfilePath,
|
||||
defaultTestProfilePath: s.defaultTestProfilePath,
|
||||
defaultPrototypeProfilePath: s.defaultPrototypeProfilePath,
|
||||
conflictResolutionProfilePath: s.conflictResolutionProfilePath,
|
||||
commitProfilePath: await getEffectiveCommitProfilePath(this.context),
|
||||
prDiffSummaryProfilePath: await getEffectivePrDiffSummaryProfilePath(this.context),
|
||||
@@ -1078,6 +1093,7 @@ export class KanbanWebviewPanel {
|
||||
autoReview: s.autoReview,
|
||||
reviewPrompt: s.reviewPrompt,
|
||||
testPrompt: s.testPrompt,
|
||||
prototypePrompt: s.prototypePrompt,
|
||||
devBranch: s.devBranch,
|
||||
releaseBranchPrefix: s.releaseBranchPrefix,
|
||||
jenkinsJob: s.jenkinsJob,
|
||||
@@ -1093,6 +1109,7 @@ export class KanbanWebviewPanel {
|
||||
defaultBrainstormProfilePath: s.defaultBrainstormProfilePath,
|
||||
defaultImplementProfilePath: s.defaultImplementProfilePath,
|
||||
defaultTestProfilePath: s.defaultTestProfilePath,
|
||||
defaultPrototypeProfilePath: s.defaultPrototypeProfilePath,
|
||||
conflictResolutionProfilePath: s.conflictResolutionProfilePath,
|
||||
commitProfilePath: await getEffectiveCommitProfilePath(this.context),
|
||||
prDiffSummaryProfilePath: await getEffectivePrDiffSummaryProfilePath(this.context),
|
||||
@@ -1202,7 +1219,7 @@ export class KanbanWebviewPanel {
|
||||
trackSessionTerminal(
|
||||
terminal: Terminal,
|
||||
issueNumber: number,
|
||||
kind: 'brainstorm' | 'implement' | 'review' | 'test',
|
||||
kind: SessionTabKind,
|
||||
): void {
|
||||
terminals.trackSessionTerminal(this, terminal, issueNumber, kind)
|
||||
}
|
||||
|
||||
@@ -63,15 +63,17 @@ describe('payloads', () => {
|
||||
handoffFrom: 'chw',
|
||||
})
|
||||
})
|
||||
it('本机墓碑:keepWorktreePath=false 时九个本机字段全清', () => {
|
||||
it('本机墓碑:keepWorktreePath=false 时本机字段全清', () => {
|
||||
expect(handoffLocalTombstones({ keepWorktreePath: false })).toEqual({
|
||||
sessionId: '',
|
||||
implementSessionId: '',
|
||||
reviewSessionId: '',
|
||||
testSessionId: '',
|
||||
prototypeSessionId: '',
|
||||
profilePath: '',
|
||||
brainstormProfilePath: '',
|
||||
testProfilePath: '',
|
||||
prototypeProfilePath: '',
|
||||
worktreePath: '',
|
||||
prDiffFile: '',
|
||||
})
|
||||
@@ -84,9 +86,11 @@ describe('payloads', () => {
|
||||
implementSessionId: '',
|
||||
reviewSessionId: '',
|
||||
testSessionId: '',
|
||||
prototypeSessionId: '',
|
||||
profilePath: '',
|
||||
brainstormProfilePath: '',
|
||||
testProfilePath: '',
|
||||
prototypeProfilePath: '',
|
||||
prDiffFile: '',
|
||||
})
|
||||
})
|
||||
@@ -99,9 +103,11 @@ describe('payloads', () => {
|
||||
implementSessionId: null,
|
||||
reviewSessionId: null,
|
||||
testSessionId: null,
|
||||
prototypeSessionId: null,
|
||||
profilePath: null,
|
||||
brainstormProfilePath: null,
|
||||
testProfilePath: null,
|
||||
prototypeProfilePath: null,
|
||||
prDiffFile: null,
|
||||
worktreePath: null,
|
||||
worktreeExists: false,
|
||||
@@ -119,9 +125,11 @@ describe('payloads', () => {
|
||||
implementSessionId: null,
|
||||
reviewSessionId: null,
|
||||
testSessionId: null,
|
||||
prototypeSessionId: null,
|
||||
profilePath: null,
|
||||
brainstormProfilePath: null,
|
||||
testProfilePath: null,
|
||||
prototypeProfilePath: null,
|
||||
prDiffFile: null,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -71,9 +71,11 @@ export function handoffStartedUiPatch(attachmentId: number, from: string, to: st
|
||||
implementSessionId: null,
|
||||
reviewSessionId: null,
|
||||
testSessionId: null,
|
||||
prototypeSessionId: null,
|
||||
profilePath: null,
|
||||
brainstormProfilePath: null,
|
||||
testProfilePath: null,
|
||||
prototypeProfilePath: null,
|
||||
prDiffFile: null,
|
||||
// worktree 没删掉时(opts.keepWorktreePath)不碰这两个字段,让 UI 继续显示孤儿 worktree。
|
||||
...(opts.keepWorktreePath ? {} : { worktreePath: null, worktreeExists: false }),
|
||||
|
||||
@@ -181,12 +181,14 @@ export async function handleHandoffStart(panel: KanbanWebviewPanel, issueNumber:
|
||||
...(str(state.sessionId) ? { sessionId: str(state.sessionId) } : {}),
|
||||
...(str(state.implementSessionId) ? { implementSessionId: str(state.implementSessionId) } : {}),
|
||||
...(str(state.testSessionId) ? { testSessionId: str(state.testSessionId) } : {}),
|
||||
...(str(state.prototypeSessionId) ? { prototypeSessionId: str(state.prototypeSessionId) } : {}),
|
||||
...(str(state.reviewSessionId) ? { reviewSessionId: str(state.reviewSessionId) } : {}),
|
||||
}
|
||||
const profiles: HandoffProfiles = {
|
||||
...(str(state.profilePath) ? { profilePath: str(state.profilePath) } : {}),
|
||||
...(str(state.brainstormProfilePath) ? { brainstormProfilePath: str(state.brainstormProfilePath) } : {}),
|
||||
...(str(state.testProfilePath) ? { testProfilePath: str(state.testProfilePath) } : {}),
|
||||
...(str(state.prototypeProfilePath) ? { prototypeProfilePath: str(state.prototypeProfilePath) } : {}),
|
||||
}
|
||||
|
||||
// ① worktree 守卫:干净 + 与远端一致。worktree 存在但 state 没有 branch 时
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -13,7 +13,7 @@ import { buildCcCommand } from '../../cc/ccCommand'
|
||||
import { buildCodexCommandString } from '../../cc/codexCommand'
|
||||
import { watchForNewCodexSession } from '../../cc/codexSessionWatcher'
|
||||
import { resolveProfilePath } from '../../cc/profiles'
|
||||
import { getBrainstormContinuePrompt, getImplementPlanPrompt, getReviewPrompt, getTestPrompt } from '../../cc/prompts'
|
||||
import { getBrainstormContinuePrompt, getImplementPlanPrompt, getPrototypePrompt, getReviewPrompt, getTestPrompt } from '../../cc/prompts'
|
||||
import { projectsDirFor, watchForNewSession } from '../../cc/sessionWatcher'
|
||||
import { detectRepo } from '../../git/remote'
|
||||
import { ensureWorktree, resolveWorktreePath } from '../../git/worktree'
|
||||
@@ -1156,7 +1156,7 @@ export async function handleStartTestSession(panel: KanbanWebviewPanel, issueNum
|
||||
}
|
||||
|
||||
// worktree 还在(PR 未合并/未清理)就进 worktree 测,已清理则回退主 worktree(main)。
|
||||
const effectiveCwd = await resolveTestSessionCwd(workspaceRoot, worktreePathRel)
|
||||
const effectiveCwd = await resolveWorktreeOrRootCwd(workspaceRoot, worktreePathRel)
|
||||
const inWorktree = effectiveCwd !== workspaceRoot
|
||||
|
||||
// 主 worktree(已合并)测试要靠 tea 了解合并进来的改动,故必须有 PR;
|
||||
@@ -1368,7 +1368,7 @@ export async function handleResumeTestSession(panel: KanbanWebviewPanel, session
|
||||
branchForHook = stateObj.branch
|
||||
if (typeof stateObj?.testProfilePath === 'string' && stateObj.testProfilePath.length > 0)
|
||||
testProfilePath = stateObj.testProfilePath
|
||||
effectiveCwd = await resolveTestSessionCwd(workspaceRoot, worktreePathRel)
|
||||
effectiveCwd = await resolveWorktreeOrRootCwd(workspaceRoot, worktreePathRel)
|
||||
}
|
||||
catch (err) {
|
||||
logger.add({
|
||||
@@ -1439,6 +1439,297 @@ export async function handleResumeTestSession(panel: KanbanWebviewPanel, session
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动一个「原型」cc 会话:详情面板点原型会话行的 ▶,开一个 claude tab 让 cc
|
||||
* 照工单正文画界面原型。
|
||||
*
|
||||
* 与测试会话的差异:原型只吃工单正文,不依赖 PR,任何阶段都能起;其余(cwd
|
||||
* 回退、profile 解析、pre-create 钩子、session id 回写)与测试会话同构。
|
||||
*
|
||||
* in-flight 锁 key `${issueNumber}:prototype`,防 createTerminal 期间重复点击。
|
||||
*/
|
||||
export async function handleStartPrototypeSession(panel: KanbanWebviewPanel, issueNumber: number): Promise<void> {
|
||||
const lockKey = `${issueNumber}:prototype`
|
||||
if (panel.resumeInFlight.has(lockKey)) {
|
||||
logger.add({
|
||||
level: 'info',
|
||||
source: 'panel',
|
||||
message: `start-prototype #${issueNumber} 已在进行中,忽略重入`,
|
||||
})
|
||||
return
|
||||
}
|
||||
panel.resumeInFlight.add(lockKey)
|
||||
|
||||
try {
|
||||
const terminalName = `issue-${issueNumber}-原型`
|
||||
const existing = panel.findExistingTerminal(terminalName)
|
||||
if (existing) {
|
||||
panel.trackSessionTerminal(existing, issueNumber, 'prototype')
|
||||
existing.show(false)
|
||||
logger.add({
|
||||
level: 'info',
|
||||
source: 'panel',
|
||||
message: `复用已有原型终端 #${issueNumber},跳过 cc 启动`,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const workspaceRoot = workspace.workspaceFolders?.[0]?.uri.fsPath
|
||||
if (!workspaceRoot) {
|
||||
void window.showErrorMessage('请先打开一个工作区文件夹')
|
||||
return
|
||||
}
|
||||
const remote = await detectRepo(workspaceRoot)
|
||||
if (!remote) {
|
||||
void window.showErrorMessage('当前工作区没有 Gitea 远程仓库')
|
||||
return
|
||||
}
|
||||
const token = await getToken(panel.context, remote.host)
|
||||
if (!token) {
|
||||
void window.showErrorMessage('请先完成 Gitea 配置')
|
||||
return
|
||||
}
|
||||
|
||||
const lockCheck = await panel.resolveLockedReason(issueNumber)
|
||||
if (lockCheck.locked) {
|
||||
void window.showWarningMessage(`等待前置工单 #${lockCheck.prerequisiteNumber} 完成`)
|
||||
return
|
||||
}
|
||||
|
||||
let prototypeProfilePath: string | undefined
|
||||
let worktreePathRel: string | undefined
|
||||
let branch: string | undefined
|
||||
try {
|
||||
const stateObj = await panel.readIssueState(issueNumber)
|
||||
if (stateObj) {
|
||||
if (typeof stateObj.prototypeProfilePath === 'string' && stateObj.prototypeProfilePath.length > 0)
|
||||
prototypeProfilePath = stateObj.prototypeProfilePath
|
||||
if (typeof stateObj.worktreePath === 'string' && stateObj.worktreePath.length > 0)
|
||||
worktreePathRel = stateObj.worktreePath
|
||||
if (typeof stateObj.branch === 'string' && stateObj.branch.length > 0)
|
||||
branch = stateObj.branch
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
logger.add({
|
||||
level: 'warn',
|
||||
source: 'panel',
|
||||
message: `读取 #${issueNumber} state JSON 失败,无法启动原型会话`,
|
||||
details: err instanceof Error ? err.message : String(err),
|
||||
})
|
||||
}
|
||||
|
||||
// worktree 还在就进 worktree 画,已清理则退回主 worktree。
|
||||
const effectiveCwd = await resolveWorktreeOrRootCwd(workspaceRoot, worktreePathRel)
|
||||
|
||||
// 原型会话只用 prototypeProfilePath;空 = DEFAULT,不回退其它会话 profile。
|
||||
const effectiveProfilePath = resolvePrototypeProfilePath(panel, prototypeProfilePath)
|
||||
if (effectiveProfilePath.includes('\'')) {
|
||||
void window.showErrorMessage(
|
||||
`启动原型失败:prototypeProfilePath 含单引号,拒绝执行 (${effectiveProfilePath})`,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const prompt = getPrototypePrompt(panel.context, { issueNumber })
|
||||
if (prompt.includes('\'')) {
|
||||
void window.showErrorMessage('启动原型失败:prompt 含单引号,拒绝执行')
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure the projects dir exists so the watcher can't miss the create
|
||||
// event. cwd 决定 claude session jsonl 落在哪个 projects 子目录下。
|
||||
const projDir = projectsDirFor(effectiveCwd)
|
||||
try {
|
||||
await fsp.mkdir(projDir, { recursive: true })
|
||||
}
|
||||
catch (err) {
|
||||
console.warn('[superpowers] failed to mkdir claude projects dir:', err)
|
||||
}
|
||||
|
||||
// Kick off the watcher before spawning the terminal so we don't race.
|
||||
const watchPromise = watchForNewSession({ projectsDir: projDir, timeoutMs: 120_000 })
|
||||
|
||||
// 原型 cc tab 创建前钩子,与实施/测试会话共用同一钩子(impl-tab-pre-create)。
|
||||
// 顶部「复用已有原型终端」的 fast path 已早 return,复用不会执行到这里。
|
||||
const settings = getSettings(panel.context)
|
||||
await panel.dispatchWorktreeHook('impl-tab-pre-create', {
|
||||
workspaceRoot,
|
||||
worktreePath: effectiveCwd,
|
||||
branch: branch ?? '',
|
||||
issueNumber,
|
||||
mainBranch: settings.devBranch || 'main',
|
||||
customScriptPath: settings.implTabPreCreateScript,
|
||||
})
|
||||
|
||||
const { themeColor, iconUri } = await panel.resolveIssueIcon(issueNumber)
|
||||
const terminal = window.createTerminal({
|
||||
name: terminalName,
|
||||
cwd: effectiveCwd,
|
||||
location: panel.resolveTerminalLocation(false),
|
||||
iconPath: iconUri,
|
||||
color: themeColor,
|
||||
})
|
||||
panel.trackSessionTerminal(terminal, issueNumber, 'prototype')
|
||||
terminal.show(false)
|
||||
logger.add({
|
||||
level: 'info',
|
||||
source: 'terminal',
|
||||
message: `已创建终端 "${terminal.name}"`,
|
||||
})
|
||||
|
||||
const cmd = buildCcCommand({ profilePath: effectiveProfilePath, prompt })
|
||||
terminal.sendText(cmd)
|
||||
logger.add({
|
||||
level: 'info',
|
||||
source: 'panel',
|
||||
message: `已发送原型 prompt 到终端 #${issueNumber}`,
|
||||
})
|
||||
|
||||
// Fire-and-forget: when the session jsonl materializes, persist the id as
|
||||
// the issue's `prototypeSessionId` so the X / resume button toggles.
|
||||
watchPromise.then(async (sid) => {
|
||||
if (!sid) {
|
||||
logger.add({
|
||||
level: 'warn',
|
||||
source: 'panel',
|
||||
message: '原型会话监听超时 (120s)',
|
||||
})
|
||||
return
|
||||
}
|
||||
logger.add({
|
||||
level: 'info',
|
||||
source: 'panel',
|
||||
message: `已捕获原型会话 ${sid}`,
|
||||
})
|
||||
try {
|
||||
await panel.mergeIssueState(issueNumber, { prototypeSessionId: sid })
|
||||
panel.postMessage({
|
||||
type: 'issue/patch',
|
||||
issueNumber,
|
||||
patch: { prototypeSessionId: sid },
|
||||
})
|
||||
}
|
||||
catch (err) {
|
||||
console.warn('[superpowers] failed to persist prototypeSessionId:', err)
|
||||
}
|
||||
}).catch((err) => {
|
||||
console.warn('[superpowers] prototype session watch failed:', err)
|
||||
})
|
||||
|
||||
void window.showInformationMessage(`已启动 #${issueNumber} 原型会话`)
|
||||
}
|
||||
finally {
|
||||
panel.resumeInFlight.delete(lockKey)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复一个已存在的原型 cc 会话:详情面板点原型会话行(prototypeTabOpen=false 时)
|
||||
* 触发。终端名 `issue-${N}-原型`、命令 `claude ... --resume ${sessionId}`。
|
||||
*
|
||||
* cwd 与 start 对称:worktree 还在就进 worktree(claude 的会话 jsonl 按 cwd 分目录,
|
||||
* 走错目录就找不到这条会话),已清理才退回主 worktree。
|
||||
* in-flight 锁 key `${issueNumber}:prototype`。
|
||||
*/
|
||||
export async function handleResumePrototypeSession(panel: KanbanWebviewPanel, sessionId: string, issueNumber: number, _relCwd?: string): Promise<void> {
|
||||
const lockKey = `${issueNumber}:prototype`
|
||||
if (panel.resumeInFlight.has(lockKey)) {
|
||||
logger.add({
|
||||
level: 'info',
|
||||
source: 'panel',
|
||||
message: `resume #${issueNumber}/prototype 已在进行中,忽略重入`,
|
||||
})
|
||||
return
|
||||
}
|
||||
panel.resumeInFlight.add(lockKey)
|
||||
|
||||
try {
|
||||
const workspaceRoot = workspace.workspaceFolders?.[0]?.uri.fsPath
|
||||
let effectiveCwd = workspaceRoot
|
||||
// 顺带读出 branch 给 impl-tab-pre-create 钩子用;读不到就空字符串。
|
||||
let branchForHook = ''
|
||||
let prototypeProfilePath: string | undefined
|
||||
if (workspaceRoot) {
|
||||
const remote = await detectRepo(workspaceRoot)
|
||||
const token = remote ? await getToken(panel.context, remote.host) : undefined
|
||||
if (remote && token) {
|
||||
try {
|
||||
const stateObj = await panel.readIssueState(issueNumber)
|
||||
const worktreePathRel = typeof stateObj?.worktreePath === 'string' ? stateObj.worktreePath : undefined
|
||||
if (stateObj && typeof stateObj.branch === 'string')
|
||||
branchForHook = stateObj.branch
|
||||
if (typeof stateObj?.prototypeProfilePath === 'string' && stateObj.prototypeProfilePath.length > 0)
|
||||
prototypeProfilePath = stateObj.prototypeProfilePath
|
||||
effectiveCwd = await resolveWorktreeOrRootCwd(workspaceRoot, worktreePathRel)
|
||||
}
|
||||
catch (err) {
|
||||
logger.add({
|
||||
level: 'warn',
|
||||
source: 'panel',
|
||||
message: `读取 #${issueNumber} state JSON 失败,原型会话回退主 worktree 恢复`,
|
||||
details: err instanceof Error ? err.message : String(err),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --settings 决定 provider/model/鉴权,resume 也必须与 start 一致解析 profile。
|
||||
const effectiveProfilePath = resolvePrototypeProfilePath(panel, prototypeProfilePath)
|
||||
if (effectiveProfilePath.includes('\'')) {
|
||||
void window.showErrorMessage(
|
||||
`resume 失败:prototypeProfilePath 含单引号,拒绝执行 (${effectiveProfilePath})`,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const terminalName = `issue-${issueNumber}-原型`
|
||||
const existingByName = panel.findExistingTerminal(terminalName)
|
||||
if (existingByName) {
|
||||
panel.trackSessionTerminal(existingByName, issueNumber, 'prototype')
|
||||
existingByName.show(false)
|
||||
return
|
||||
}
|
||||
|
||||
// 回退主 worktree 时不触发 pre-create——主 worktree 不该被 pycharm 指向,
|
||||
// 与实施 / 测试 resume 一致。
|
||||
if (effectiveCwd !== workspaceRoot && workspaceRoot) {
|
||||
const settings = getSettings(panel.context)
|
||||
await panel.dispatchWorktreeHook('impl-tab-pre-create', {
|
||||
workspaceRoot,
|
||||
worktreePath: effectiveCwd!,
|
||||
branch: branchForHook,
|
||||
issueNumber,
|
||||
mainBranch: settings.devBranch || 'main',
|
||||
customScriptPath: settings.implTabPreCreateScript,
|
||||
})
|
||||
}
|
||||
|
||||
const { themeColor, iconUri } = await panel.resolveIssueIcon(issueNumber)
|
||||
const terminal = window.createTerminal({
|
||||
name: terminalName,
|
||||
cwd: effectiveCwd,
|
||||
location: panel.resolveTerminalLocation(false),
|
||||
iconPath: iconUri,
|
||||
color: themeColor,
|
||||
})
|
||||
panel.trackSessionTerminal(terminal, issueNumber, 'prototype')
|
||||
terminal.show(false)
|
||||
logger.add({
|
||||
level: 'info',
|
||||
source: 'terminal',
|
||||
message: `已创建原型会话终端 #${issueNumber} cwd=${effectiveCwd}`,
|
||||
})
|
||||
|
||||
const cmd = buildCcCommand({ profilePath: effectiveProfilePath, resumeSessionId: sessionId })
|
||||
terminal.sendText(cmd)
|
||||
}
|
||||
finally {
|
||||
panel.resumeInFlight.delete(lockKey)
|
||||
panel.postMessage({ type: 'session/tab-settled', sessionId })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 当 done 列的 PR merge 因冲突失败时调用:在主 workspace 检出 feature 分支
|
||||
* 并 merge dev 让冲突落地到 working tree,然后启一个临时 cc 会话让 cc 解决
|
||||
@@ -1696,13 +1987,25 @@ export function resolveTestProfilePath(panel: KanbanWebviewPanel, testProfilePat
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试会话的工作目录:worktree 还在就用 worktree,已清理则回退主 worktree。
|
||||
* 原型会话 profile:工单级 `prototypeProfilePath` > 设置里的「原型默认配置文件」
|
||||
* > DEFAULT。与其它三种会话互不回退。
|
||||
*/
|
||||
export function resolvePrototypeProfilePath(panel: KanbanWebviewPanel, prototypeProfilePath: string | undefined): string {
|
||||
return resolveProfilePath(
|
||||
prototypeProfilePath?.trim()
|
||||
|| getSettings(panel.context).defaultPrototypeProfilePath.trim()
|
||||
|| undefined,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试 / 原型会话的工作目录:worktree 还在就用 worktree,已清理则回退主 worktree。
|
||||
*
|
||||
* @param workspaceRoot 主 worktree 绝对路径
|
||||
* @param worktreePathRel state JSON 里的 worktree 相对路径(可空)
|
||||
* @returns 实际用作终端 cwd 的绝对路径
|
||||
*/
|
||||
async function resolveTestSessionCwd(workspaceRoot: string, worktreePathRel?: string): Promise<string> {
|
||||
async function resolveWorktreeOrRootCwd(workspaceRoot: string, worktreePathRel?: string): Promise<string> {
|
||||
if (worktreePathRel) {
|
||||
const abs = resolveWorktreePath(worktreePathRel, workspaceRoot)
|
||||
try {
|
||||
|
||||
@@ -28,6 +28,7 @@ export async function handleSettingsSave(panel: KanbanWebviewPanel, payload: {
|
||||
autoReview: boolean
|
||||
reviewPrompt: string
|
||||
testPrompt: string
|
||||
prototypePrompt: string
|
||||
devBranch: string
|
||||
releaseBranchPrefix: string
|
||||
jenkinsJob: string
|
||||
@@ -43,6 +44,7 @@ export async function handleSettingsSave(panel: KanbanWebviewPanel, payload: {
|
||||
defaultBrainstormProfilePath: string
|
||||
defaultImplementProfilePath: string
|
||||
defaultTestProfilePath: string
|
||||
defaultPrototypeProfilePath: string
|
||||
conflictResolutionProfilePath: string
|
||||
commitProfilePath: string
|
||||
prDiffSummaryProfilePath: string
|
||||
@@ -78,10 +80,11 @@ export async function handleSettingsSave(panel: KanbanWebviewPanel, payload: {
|
||||
const trimmedPreRemove = payload.worktreePreRemoveScript.trim()
|
||||
const trimmedImplPre = payload.implTabPreCreateScript.trim()
|
||||
const trimmedImplPost = payload.implTabPostCloseScript.trim()
|
||||
// 三个会话默认 profile:'' 有意义(= 落到 DEFAULT_PROFILE_PATH),只 trim。
|
||||
// 四个会话默认 profile:'' 有意义(= 落到 DEFAULT_PROFILE_PATH),只 trim。
|
||||
const trimmedDefaultBrainstormProfile = payload.defaultBrainstormProfilePath.trim()
|
||||
const trimmedDefaultImplementProfile = payload.defaultImplementProfilePath.trim()
|
||||
const trimmedDefaultTestProfile = payload.defaultTestProfilePath.trim()
|
||||
const trimmedDefaultPrototypeProfile = payload.defaultPrototypeProfilePath.trim()
|
||||
// conflict-resolution profile: '' is meaningful (= DEFAULT_PROFILE_PATH).
|
||||
const trimmedConflictProfile = payload.conflictResolutionProfilePath.trim()
|
||||
// 提交代码 profile:'' 有意义(= 未配置 → 提交严格失败),只 trim。
|
||||
@@ -115,6 +118,7 @@ export async function handleSettingsSave(panel: KanbanWebviewPanel, payload: {
|
||||
autoReview: payload.autoReview,
|
||||
reviewPrompt: payload.reviewPrompt || prev.reviewPrompt,
|
||||
testPrompt: payload.testPrompt || prev.testPrompt,
|
||||
prototypePrompt: payload.prototypePrompt || prev.prototypePrompt,
|
||||
devBranch: trimmedDevBranch || prev.devBranch,
|
||||
releaseBranchPrefix: trimmedReleasePrefix || prev.releaseBranchPrefix,
|
||||
jenkinsJob: trimmedJenkinsJob,
|
||||
@@ -130,6 +134,7 @@ export async function handleSettingsSave(panel: KanbanWebviewPanel, payload: {
|
||||
defaultBrainstormProfilePath: trimmedDefaultBrainstormProfile,
|
||||
defaultImplementProfilePath: trimmedDefaultImplementProfile,
|
||||
defaultTestProfilePath: trimmedDefaultTestProfile,
|
||||
defaultPrototypeProfilePath: trimmedDefaultPrototypeProfile,
|
||||
conflictResolutionProfilePath: trimmedConflictProfile,
|
||||
commitProfilePath: trimmedCommitProfile,
|
||||
prDiffSummaryProfilePath: trimmedPrDiffSummaryProfile,
|
||||
@@ -148,6 +153,7 @@ export async function handleSettingsSave(panel: KanbanWebviewPanel, payload: {
|
||||
autoReview: payload.autoReview,
|
||||
reviewPrompt: payload.reviewPrompt,
|
||||
testPrompt: payload.testPrompt,
|
||||
prototypePrompt: payload.prototypePrompt,
|
||||
devBranch: trimmedDevBranch,
|
||||
releaseBranchPrefix: trimmedReleasePrefix,
|
||||
jenkinsJob: trimmedJenkinsJob,
|
||||
@@ -163,6 +169,7 @@ export async function handleSettingsSave(panel: KanbanWebviewPanel, payload: {
|
||||
defaultBrainstormProfilePath: trimmedDefaultBrainstormProfile,
|
||||
defaultImplementProfilePath: trimmedDefaultImplementProfile,
|
||||
defaultTestProfilePath: trimmedDefaultTestProfile,
|
||||
defaultPrototypeProfilePath: trimmedDefaultPrototypeProfile,
|
||||
conflictResolutionProfilePath: trimmedConflictProfile,
|
||||
commitProfilePath: trimmedCommitProfile,
|
||||
prDiffSummaryProfilePath: trimmedPrDiffSummaryProfile,
|
||||
@@ -258,6 +265,7 @@ export async function handleEditSettingsRequest(panel: KanbanWebviewPanel): Prom
|
||||
autoReview: s.autoReview,
|
||||
reviewPrompt: s.reviewPrompt,
|
||||
testPrompt: s.testPrompt,
|
||||
prototypePrompt: s.prototypePrompt,
|
||||
devBranch: s.devBranch,
|
||||
releaseBranchPrefix: s.releaseBranchPrefix,
|
||||
jenkinsJob: s.jenkinsJob,
|
||||
@@ -273,6 +281,7 @@ export async function handleEditSettingsRequest(panel: KanbanWebviewPanel): Prom
|
||||
defaultBrainstormProfilePath: s.defaultBrainstormProfilePath,
|
||||
defaultImplementProfilePath: s.defaultImplementProfilePath,
|
||||
defaultTestProfilePath: s.defaultTestProfilePath,
|
||||
defaultPrototypeProfilePath: s.defaultPrototypeProfilePath,
|
||||
conflictResolutionProfilePath: s.conflictResolutionProfilePath,
|
||||
commitProfilePath,
|
||||
prDiffSummaryProfilePath,
|
||||
|
||||
@@ -9,6 +9,9 @@ import { logger } from '../../logging/logger'
|
||||
import { getSettings } from '../../settings/store'
|
||||
import { existingTerminalGroupColumn, terminalEditorColumn } from '../terminalColumn'
|
||||
|
||||
/** 一个工单能开的会话终端种类;每种对应详情面板里的一行会话 id。 */
|
||||
export type SessionTabKind = 'brainstorm' | 'implement' | 'review' | 'test' | 'prototype'
|
||||
|
||||
export function resolveTerminalLocation(panel: KanbanWebviewPanel | null, preserveFocus: boolean): TerminalEditorLocationOptions {
|
||||
if (!panel) {
|
||||
return {
|
||||
@@ -155,13 +158,14 @@ export function handleSessionFocus(panel: KanbanWebviewPanel, issueNumber: numbe
|
||||
newIssueTerm.show(true)
|
||||
return
|
||||
}
|
||||
// Priority 1-4: 实施 > 规划 > 审查 > 测试. Match by terminal.name since we know
|
||||
// the convention (issue-${N}-实施 / issue-${N}-规划 / issue-${N}-审查 / issue-${N}-测试).
|
||||
// Priority 1-5: 实施 > 规划 > 审查 > 测试 > 原型. Match by terminal.name since we
|
||||
// know the convention (issue-${N}-实施 / -规划 / -审查 / -测试 / -原型).
|
||||
const namePriority = [
|
||||
`issue-${issueNumber}-实施`,
|
||||
`issue-${issueNumber}-规划`,
|
||||
`issue-${issueNumber}-审查`,
|
||||
`issue-${issueNumber}-测试`,
|
||||
`issue-${issueNumber}-原型`,
|
||||
]
|
||||
for (const name of namePriority) {
|
||||
const term = findExistingTerminal(panel, name)
|
||||
@@ -181,12 +185,12 @@ export function handleSessionFocus(panel: KanbanWebviewPanel, issueNumber: numbe
|
||||
* 解析 issueNumber 并通知 webview 选中对应工单。去重与回声抑制由调用方的
|
||||
* `FocusedTerminalTracker` 负责,这里每次调用都视为一次真实切换。
|
||||
*
|
||||
* 终端名按 `issue-${N}-(规划|实施|审查|测试)` 命名;`issue-new-${nonce}-...`
|
||||
* 终端名按 `issue-${N}-(规划|实施|审查|测试|原型)` 命名;`issue-new-${nonce}-...`
|
||||
* 是新建工单流程的占位 tab,没有 issue number,跳过。
|
||||
*/
|
||||
export function handleActiveTerminalChanged(panel: KanbanWebviewPanel, terminal: Terminal): void {
|
||||
// Primary: `issue-${N}-(规划|实施|审查|测试)` — the steady-state naming.
|
||||
const m = terminal.name.match(/^issue-(\d+)-(规划|实施|审查|测试)/)
|
||||
// Primary: `issue-${N}-(规划|实施|审查|测试|原型)` — the steady-state naming.
|
||||
const m = terminal.name.match(/^issue-(\d+)-(规划|实施|审查|测试|原型)/)
|
||||
if (m) {
|
||||
const issueNumber = Number.parseInt(m[1], 10)
|
||||
if (Number.isFinite(issueNumber)) {
|
||||
@@ -220,8 +224,8 @@ export function handleActiveTerminalChanged(panel: KanbanWebviewPanel, terminal:
|
||||
* 按钮能找到对应 terminal。
|
||||
*/
|
||||
export function withLiveTerminalTabState(panel: KanbanWebviewPanel, issues: Issue[]): Issue[] {
|
||||
const liveByIssue = new Map<number, Partial<Pick<Issue, 'brainstormTabOpen' | 'implementTabOpen' | 'reviewTabOpen' | 'testTabOpen'>>>()
|
||||
const mark = (issueNumber: number, kind: 'brainstorm' | 'implement' | 'review' | 'test'): void => {
|
||||
const liveByIssue = new Map<number, Partial<Pick<Issue, 'brainstormTabOpen' | 'implementTabOpen' | 'reviewTabOpen' | 'testTabOpen' | 'prototypeTabOpen'>>>()
|
||||
const mark = (issueNumber: number, kind: SessionTabKind): void => {
|
||||
const patch = liveByIssue.get(issueNumber) ?? {}
|
||||
if (kind === 'brainstorm')
|
||||
patch.brainstormTabOpen = true
|
||||
@@ -229,8 +233,10 @@ export function withLiveTerminalTabState(panel: KanbanWebviewPanel, issues: Issu
|
||||
patch.implementTabOpen = true
|
||||
else if (kind === 'review')
|
||||
patch.reviewTabOpen = true
|
||||
else
|
||||
else if (kind === 'test')
|
||||
patch.testTabOpen = true
|
||||
else
|
||||
patch.prototypeTabOpen = true
|
||||
liveByIssue.set(issueNumber, patch)
|
||||
}
|
||||
|
||||
@@ -242,19 +248,21 @@ export function withLiveTerminalTabState(panel: KanbanWebviewPanel, issues: Issu
|
||||
for (const terminal of window.terminals) {
|
||||
if (terminal.exitStatus !== undefined)
|
||||
continue
|
||||
const match = terminal.name.match(/^issue-(\d+)-(规划|实施|审查|测试)(?:\s|$)/)
|
||||
const match = terminal.name.match(/^issue-(\d+)-(规划|实施|审查|测试|原型)(?:\s|$)/)
|
||||
if (!match)
|
||||
continue
|
||||
const issueNumber = Number.parseInt(match[1], 10)
|
||||
if (!Number.isFinite(issueNumber))
|
||||
continue
|
||||
const kind = match[2] === '规划'
|
||||
const kind: SessionTabKind = match[2] === '规划'
|
||||
? 'brainstorm'
|
||||
: match[2] === '实施'
|
||||
? 'implement'
|
||||
: match[2] === '审查'
|
||||
? 'review'
|
||||
: 'test'
|
||||
: match[2] === '测试'
|
||||
? 'test'
|
||||
: 'prototype'
|
||||
panel.terminalOrigin.set(terminal, { issueNumber, kind })
|
||||
mark(issueNumber, kind)
|
||||
}
|
||||
@@ -280,7 +288,7 @@ export function trackSessionTerminal(
|
||||
panel: KanbanWebviewPanel,
|
||||
terminal: Terminal,
|
||||
issueNumber: number,
|
||||
kind: 'brainstorm' | 'implement' | 'review' | 'test',
|
||||
kind: SessionTabKind,
|
||||
): void {
|
||||
panel.terminalOrigin.set(terminal, { issueNumber, kind })
|
||||
panel.postMessage({
|
||||
@@ -293,7 +301,9 @@ export function trackSessionTerminal(
|
||||
? { implementTabOpen: true }
|
||||
: kind === 'review'
|
||||
? { reviewTabOpen: true }
|
||||
: { testTabOpen: true },
|
||||
: kind === 'test'
|
||||
? { testTabOpen: true }
|
||||
: { prototypeTabOpen: true },
|
||||
})
|
||||
}
|
||||
|
||||
@@ -319,7 +329,9 @@ export function untrackClosedTerminal(panel: KanbanWebviewPanel, closed: Termina
|
||||
? { implementTabOpen: false }
|
||||
: origin.kind === 'review'
|
||||
? { reviewTabOpen: false }
|
||||
: { testTabOpen: false },
|
||||
: origin.kind === 'test'
|
||||
? { testTabOpen: false }
|
||||
: { prototypeTabOpen: false },
|
||||
})
|
||||
}
|
||||
|
||||
@@ -334,7 +346,7 @@ export function untrackClosedTerminal(panel: KanbanWebviewPanel, closed: Termina
|
||||
* thread_id 索引,此路径还没 thread_id),但会被登记到 terminalOrigin,
|
||||
* 所以用它做单一真相源最稳。
|
||||
*/
|
||||
export function handleCloseSessionTab(panel: KanbanWebviewPanel, issueNumber: number, kind: 'brainstorm' | 'implement' | 'review' | 'test'): void {
|
||||
export function handleCloseSessionTab(panel: KanbanWebviewPanel, issueNumber: number, kind: SessionTabKind): void {
|
||||
let terminal: Terminal | undefined
|
||||
for (const [t, origin] of panel.terminalOrigin) {
|
||||
if (origin.issueNumber === issueNumber && origin.kind === kind) {
|
||||
|
||||
@@ -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, 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, profilePath?: string | null, brainstormProfilePath?: string | null, testProfilePath?: string | null, assignees?: string[], handoffAttachmentId?: string | null, handoffFrom?: string | null } }
|
||||
| { 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 } }
|
||||
/** 某个会话 id 的开 tab 请求已处理完(开成、复用或被拒都算),webview 据此停掉行内 spinner。 */
|
||||
| { type: 'session/tab-settled', sessionId: string }
|
||||
| { type: 'issue/pr-diff-summary-done', issueNumber: number }
|
||||
@@ -77,6 +77,7 @@ export type ExtensionToWebview
|
||||
autoReview: boolean
|
||||
reviewPrompt: string
|
||||
testPrompt: string
|
||||
prototypePrompt: string
|
||||
devBranch: string
|
||||
releaseBranchPrefix: string
|
||||
jenkinsJob: string
|
||||
@@ -92,6 +93,7 @@ export type ExtensionToWebview
|
||||
defaultBrainstormProfilePath: string
|
||||
defaultImplementProfilePath: string
|
||||
defaultTestProfilePath: string
|
||||
defaultPrototypeProfilePath: string
|
||||
conflictResolutionProfilePath: string
|
||||
commitProfilePath: string
|
||||
prDiffSummaryProfilePath: string
|
||||
@@ -160,6 +162,7 @@ export type WebviewToExtension
|
||||
autoReview: boolean
|
||||
reviewPrompt: string
|
||||
testPrompt: string
|
||||
prototypePrompt: string
|
||||
devBranch: string
|
||||
releaseBranchPrefix: string
|
||||
jenkinsJob: string
|
||||
@@ -175,6 +178,7 @@ export type WebviewToExtension
|
||||
defaultBrainstormProfilePath: string
|
||||
defaultImplementProfilePath: string
|
||||
defaultTestProfilePath: string
|
||||
defaultPrototypeProfilePath: string
|
||||
conflictResolutionProfilePath: string
|
||||
commitProfilePath: string
|
||||
prDiffSummaryProfilePath: string
|
||||
@@ -195,8 +199,10 @@ export type WebviewToExtension
|
||||
| { type: 'session/focus', issueNumber: number }
|
||||
| { type: 'session/resume-review', sessionId: string, issueNumber: number, cwd?: string }
|
||||
| { type: 'session/resume-test', sessionId: string, issueNumber: number, cwd?: string }
|
||||
| { type: 'session/resume-prototype', sessionId: string, issueNumber: number, cwd?: string }
|
||||
| { type: 'session/start-review', issueNumber: number }
|
||||
| { type: 'session/start-test', issueNumber: number }
|
||||
| { type: 'session/start-prototype', issueNumber: number }
|
||||
| { type: 'editor/open-file', path: string }
|
||||
| { type: 'profiles/list' }
|
||||
| { type: 'issue/implement', issueNumber: number, planFile: string, profilePath?: string, sessionId?: string }
|
||||
@@ -220,10 +226,11 @@ export type WebviewToExtension
|
||||
| { type: 'issue/update-brainstorm-profile-path', issueNumber: number, brainstormProfilePath: string }
|
||||
| { type: 'issue/clear-brainstorm-session', issueNumber: number }
|
||||
| { type: 'issue/update-test-profile-path', issueNumber: number, testProfilePath: string }
|
||||
| { type: 'issue/update-prototype-profile-path', issueNumber: number, prototypeProfilePath: string }
|
||||
| { type: 'logs/fetch' }
|
||||
| { type: 'logs/clear' }
|
||||
| { type: 'commit/run' }
|
||||
| { type: 'session/close-tab', issueNumber: number, kind: 'brainstorm' | 'implement' | 'review' | 'test' }
|
||||
| { type: 'session/close-tab', issueNumber: number, kind: 'brainstorm' | 'implement' | 'review' | 'test' | 'prototype' }
|
||||
| { type: 'release/check' }
|
||||
| { type: 'release/cut' }
|
||||
| { type: 'release/cherry-pick' }
|
||||
|
||||
@@ -14,8 +14,10 @@ export const OWNER_ONLY_MESSAGE_TYPES: ReadonlySet<string> = new Set([
|
||||
'session/focus',
|
||||
'session/resume-review',
|
||||
'session/resume-test',
|
||||
'session/resume-prototype',
|
||||
'session/start-review',
|
||||
'session/start-test',
|
||||
'session/start-prototype',
|
||||
'session/close-tab',
|
||||
'brainstorm/continue',
|
||||
'issue/implement',
|
||||
@@ -29,6 +31,7 @@ export const OWNER_ONLY_MESSAGE_TYPES: ReadonlySet<string> = new Set([
|
||||
'issue/update-brainstorm-profile-path',
|
||||
'issue/clear-brainstorm-session',
|
||||
'issue/update-test-profile-path',
|
||||
'issue/update-prototype-profile-path',
|
||||
'dependency/set',
|
||||
'dependency/clear',
|
||||
'handoff/users',
|
||||
|
||||
@@ -34,7 +34,9 @@ const FALLBACK_TEST_PROMPT = `/pr-acceptance-testing 验收 {issueNumber} 号工
|
||||
|
||||
const FALLBACK_REVIEW_PROMPT = `用 tea 拿到 #{prNumber} PR,diff 交给 ecc:code-reviewer 子代理审查。审查意见用 spx pr review-comment 发评论,body 第一行写 <!-- spx:review=1 -->。`
|
||||
|
||||
type PromptName = 'brainstorm' | 'brainstorm-continue' | 'implement-plan' | 'review' | 'test'
|
||||
const FALLBACK_PROTOTYPE_PROMPT = `/ecc:figma-plugin-prototyping 给 {issueNumber} 号工单画原型:先读工单正文锁定要画的界面和状态,再写本地 Figma 插件把画板和点击流做出来。画完汇报插件路径和怎么导入,不要改业务代码。`
|
||||
|
||||
type PromptName = 'brainstorm' | 'brainstorm-continue' | 'implement-plan' | 'review' | 'test' | 'prototype'
|
||||
|
||||
/**
|
||||
* Read a default prompt template from `${extensionPath}/prompts/${name}.md`.
|
||||
@@ -57,6 +59,7 @@ export function readDefaultPrompt(extensionPath: string, name: PromptName): stri
|
||||
case 'implement-plan': return FALLBACK_IMPLEMENT_PLAN_PROMPT
|
||||
case 'review': return FALLBACK_REVIEW_PROMPT
|
||||
case 'test': return FALLBACK_TEST_PROMPT
|
||||
case 'prototype': return FALLBACK_PROTOTYPE_PROMPT
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -92,6 +95,8 @@ export interface Settings {
|
||||
reviewPrompt: string
|
||||
/** 测试会话首个提示词模板;占位符 {issueNumber} / {prRef}。 */
|
||||
testPrompt: string
|
||||
/** 原型会话首个提示词模板;占位符 {issueNumber}。 */
|
||||
prototypePrompt: string
|
||||
/** Day-to-day development branch (e.g. `main`). Jenkins doesn't watch it. */
|
||||
devBranch: string
|
||||
/**
|
||||
@@ -194,6 +199,8 @@ export interface Settings {
|
||||
defaultImplementProfilePath: string
|
||||
/** 测试会话的默认 Claude profile;工单级「测试配置文件」为空时用它。 */
|
||||
defaultTestProfilePath: string
|
||||
/** 原型会话的默认 Claude profile;工单级「原型配置文件」为空时用它。 */
|
||||
defaultPrototypeProfilePath: string
|
||||
/**
|
||||
* Claude settings profile path for the conflict-resolution cc session
|
||||
* (drag to 完成 when PR merge conflicts). Empty = DEFAULT_PROFILE_PATH.
|
||||
@@ -272,6 +279,7 @@ function defaults(ctx: ExtensionContext): Settings {
|
||||
autoReview: true,
|
||||
reviewPrompt: readDefaultPrompt(ctx.extensionPath, 'review'),
|
||||
testPrompt: readDefaultPrompt(ctx.extensionPath, 'test'),
|
||||
prototypePrompt: readDefaultPrompt(ctx.extensionPath, 'prototype'),
|
||||
devBranch: 'main',
|
||||
releaseBranchPrefix: 'release/',
|
||||
jenkinsJob: '',
|
||||
@@ -290,6 +298,7 @@ function defaults(ctx: ExtensionContext): Settings {
|
||||
defaultBrainstormProfilePath: '',
|
||||
defaultImplementProfilePath: '',
|
||||
defaultTestProfilePath: '',
|
||||
defaultPrototypeProfilePath: '',
|
||||
conflictResolutionProfilePath: '',
|
||||
commitProfilePath: '',
|
||||
prDiffSummaryProfilePath: '',
|
||||
@@ -328,6 +337,9 @@ export function getSettings(ctx: ExtensionContext): Settings {
|
||||
const testPrompt = typeof stored.testPrompt === 'string' && stored.testPrompt.length > 0
|
||||
? stored.testPrompt
|
||||
: base.testPrompt
|
||||
const prototypePrompt = typeof stored.prototypePrompt === 'string' && stored.prototypePrompt.length > 0
|
||||
? stored.prototypePrompt
|
||||
: base.prototypePrompt
|
||||
// devBranch always has a usable value — fall back to 'main' if missing/blank
|
||||
// so we never end up trying to fetch the empty string.
|
||||
const devBranch = typeof stored.devBranch === 'string' && stored.devBranch.length > 0
|
||||
@@ -390,7 +402,7 @@ export function getSettings(ctx: ExtensionContext): Settings {
|
||||
const youtrackCloseCommand = typeof stored.youtrackCloseCommand === 'string'
|
||||
? stored.youtrackCloseCommand
|
||||
: base.youtrackCloseCommand
|
||||
// 三个会话默认 profile:'' 有意义(= 落到 DEFAULT_PROFILE_PATH),不强制回退。
|
||||
// 四个会话默认 profile:'' 有意义(= 落到 DEFAULT_PROFILE_PATH),不强制回退。
|
||||
const defaultBrainstormProfilePath = typeof stored.defaultBrainstormProfilePath === 'string'
|
||||
? stored.defaultBrainstormProfilePath
|
||||
: base.defaultBrainstormProfilePath
|
||||
@@ -400,6 +412,9 @@ export function getSettings(ctx: ExtensionContext): Settings {
|
||||
const defaultTestProfilePath = typeof stored.defaultTestProfilePath === 'string'
|
||||
? stored.defaultTestProfilePath
|
||||
: base.defaultTestProfilePath
|
||||
const defaultPrototypeProfilePath = typeof stored.defaultPrototypeProfilePath === 'string'
|
||||
? stored.defaultPrototypeProfilePath
|
||||
: base.defaultPrototypeProfilePath
|
||||
// conflict-resolution profile: '' is meaningful (= DEFAULT_PROFILE_PATH at
|
||||
// launch time), so don't coerce. Non-string legacy values collapse to ''.
|
||||
const conflictResolutionProfilePath = typeof stored.conflictResolutionProfilePath === 'string'
|
||||
@@ -434,6 +449,7 @@ export function getSettings(ctx: ExtensionContext): Settings {
|
||||
autoReview,
|
||||
reviewPrompt,
|
||||
testPrompt,
|
||||
prototypePrompt,
|
||||
devBranch,
|
||||
releaseBranchPrefix,
|
||||
jenkinsJob,
|
||||
@@ -452,6 +468,7 @@ export function getSettings(ctx: ExtensionContext): Settings {
|
||||
defaultBrainstormProfilePath,
|
||||
defaultImplementProfilePath,
|
||||
defaultTestProfilePath,
|
||||
defaultPrototypeProfilePath,
|
||||
conflictResolutionProfilePath,
|
||||
commitProfilePath,
|
||||
prDiffSummaryProfilePath,
|
||||
|
||||
@@ -68,9 +68,11 @@ function toIssue(baseUrl: string, it: YouTrackIssue): Issue {
|
||||
implementSessionId: str(state.implementSessionId),
|
||||
reviewSessionId: str(state.reviewSessionId),
|
||||
testSessionId: str(state.testSessionId),
|
||||
prototypeSessionId: str(state.prototypeSessionId),
|
||||
profilePath: str(state.profilePath),
|
||||
brainstormProfilePath: str(state.brainstormProfilePath),
|
||||
testProfilePath: str(state.testProfilePath),
|
||||
prototypeProfilePath: str(state.prototypeProfilePath),
|
||||
specFile: str(state.specFile),
|
||||
planFile: str(state.planFile),
|
||||
prDiffFile: str(state.prDiffFile),
|
||||
|
||||
@@ -20,7 +20,7 @@ import { addComment, listComments } from './api'
|
||||
export const STATE_MARKER = '<!--spx-state-->'
|
||||
|
||||
/** Same field set as the Gitea state blob — kept independent to avoid coupling. */
|
||||
const KNOWN_STATE_FIELDS = ['column', 'sessionId', 'implementSessionId', 'reviewSessionId', 'testSessionId', 'profilePath', 'brainstormProfilePath', 'testProfilePath', 'specFile', 'planFile', 'prDiffFile', 'pr', 'prMerged', 'prMergedAt', 'branch', 'worktreePath', 'implementStatus', 'color', 'autoReview', 'handoffAttachmentId', 'handoffFrom'] as const
|
||||
const KNOWN_STATE_FIELDS = ['column', 'sessionId', 'implementSessionId', 'reviewSessionId', 'testSessionId', 'prototypeSessionId', 'profilePath', 'brainstormProfilePath', 'testProfilePath', 'prototypeProfilePath', 'specFile', 'planFile', 'prDiffFile', 'pr', 'prMerged', 'prMergedAt', 'branch', 'worktreePath', 'implementStatus', 'color', 'autoReview', 'handoffAttachmentId', 'handoffFrom'] as const
|
||||
|
||||
/**
|
||||
* Parse a comment body into a state object, or null if it isn't a state
|
||||
|
||||
Reference in new issue
Block a user