feat(vscode): 头脑风暴 profile 独立 + codex 命令统一组装收尾

- 头脑风暴/实施/测试会话 profile 彻底独立,互不回退,各自空值回落默认
- 新建工单弹窗的 profile 选择改为头脑风暴专用 brainstormProfilePath
- 详情面板新增「头脑风暴配置文件」下拉
- 删除从未接线的死代码 reviewFlow.ts(runReview 全仓库无调用方)
- codexCommand 精简为仅 resume/interactive 两种终端命令形态
- 版本号 0.2.55 → 0.2.60
This commit is contained in:
2026-07-13 07:50:39 +08:00
parent 5188c23765
commit b585016cd7
22 changed files with 258 additions and 366 deletions
+8 -18
View File
@@ -99,13 +99,13 @@ export class KanbanWebviewPanel {
* terminal is shown; the session watcher fills in `sessionId` once cc
* starts writing its jsonl. The webhook coordinator drains entries via
* `takePendingIssueCreation` when the corresponding `issues opened`
* payload arrives, then merges the column / sessionId / profilePath /
* payload arrives, then merges the column / sessionId / brainstormProfilePath /
* color into the state-JSON comment and cleans up the inbox tmpdir.
*/
// internal: handler 模块访问
readonly pendingIssueCreations = new Map<string, {
sessionId?: string
profilePath?: string
brainstormProfilePath?: string
/** Palette id (e.g. `terminal.ansiBlue`) — same shape stored in state JSON. */
color: string
workspaceRoot: string
@@ -372,7 +372,7 @@ export class KanbanWebviewPanel {
return
}
if (msg.type === 'issue/create') {
void issues.handleIssueCreate(this, msg.userRequest, msg.images, msg.profilePath)
void issues.handleIssueCreate(this, msg.userRequest, msg.images, msg.brainstormProfilePath)
return
}
if (msg.type === 'profiles/list') {
@@ -470,6 +470,10 @@ export class KanbanWebviewPanel {
void issues.handleUpdateProfilePath(this, msg.issueNumber, msg.profilePath)
return
}
if (msg.type === 'issue/update-brainstorm-profile-path') {
void issues.handleUpdateBrainstormProfilePath(this, msg.issueNumber, msg.brainstormProfilePath)
return
}
if (msg.type === 'issue/update-test-profile-path') {
void issues.handleUpdateTestProfilePath(this, msg.issueNumber, msg.testProfilePath)
return
@@ -931,20 +935,6 @@ export class KanbanWebviewPanel {
}
}
/**
* Pick the profile.json that implement-class cc sessions
* (handleImplement / handleResumeSession when sessionKind === 'implement')
* should launch with. Priority:
* 1. The per-issue `profilePath` recorded in state JSON (preserves the
* brainstorm-time choice, overridable from the issue detail panel).
* 2. The hard-coded `DEFAULT_PROFILE_PATH` fallback.
*
* Conflict-resolution sessions use the global
* `settings.conflictResolutionProfilePath` instead of this helper.
* Brainstorm sessions deliberately don't go through this helper — they
* keep the legacy `profilePath || DEFAULT_PROFILE_PATH` so creators can
* still pick a profile per issue at brainstorm time.
*/
/**
* Resolve an issue number to its source identity, for routing state
* persistence. Unknown numbers default to gitea (back-compat / pre-load).
@@ -1058,7 +1048,7 @@ export class KanbanWebviewPanel {
// internal: handler 模块访问
takePendingIssueCreation(nonce: string): {
sessionId?: string
profilePath?: string
brainstormProfilePath?: string
color: string
workspaceRoot: string
inboxDir: string
+98 -12
View File
@@ -1007,7 +1007,7 @@ export async function handleIssueCreate(
panel: KanbanWebviewPanel,
userRequest: string,
images?: Array<{ mediaType: string, base64: string }>,
profilePath?: string,
brainstormProfilePath?: string,
): Promise<void> {
const trimmed = userRequest.trim()
if (!trimmed) {
@@ -1141,13 +1141,13 @@ export async function handleIssueCreate(
}
const effectiveProfilePath
= profilePath && profilePath.trim() !== '' ? profilePath : DEFAULT_PROFILE_PATH
= brainstormProfilePath && brainstormProfilePath.trim() !== '' ? brainstormProfilePath : DEFAULT_PROFILE_PATH
if (effectiveProfilePath.includes('\'')) {
panel.postMessage({
type: 'toast/show',
id: makeNonce(),
level: 'error',
message: `创建失败:profilePath 含单引号,拒绝执行 (${effectiveProfilePath})`,
message: `创建失败:brainstormProfilePath 含单引号,拒绝执行 (${effectiveProfilePath})`,
dismissOnTimer: 8000,
})
try {
@@ -1185,8 +1185,12 @@ export async function handleIssueCreate(
message: `已创建终端 "${terminal.name}"`,
})
// 新建工单会话是头脑风暴;只把用户选中的 brainstorm profile 写入 pending
// 空串不落盘(webhook 侧也会跳过空值),实施/测试 profile 各自独立配置。
const storedBrainstormProfile
= brainstormProfilePath && brainstormProfilePath.trim() !== '' ? brainstormProfilePath.trim() : undefined
panel.pendingIssueCreations.set(nonce, {
profilePath: effectiveProfilePath,
brainstormProfilePath: storedBrainstormProfile,
color,
workspaceRoot,
inboxDir,
@@ -1572,10 +1576,10 @@ export async function handleUpdateAutoReview(panel: KanbanWebviewPanel, issueNum
/**
* Persist a per-issue `profilePath` override into the issue's state JSON
* comment. The implement / implement-resume / conflict-resolution cc sessions
* launch with this profile (see resolveImplementProfilePath); empty falls back
* to DEFAULT_PROFILE_PATH. Mirrors handleUpdateAutoReview: optimistic update on
* the webview, rolled back via `issue/patch` if the persist fails.
* comment. Implement / implement-resume sessions use this profile
* (see resolveImplementProfilePath); empty = DEFAULT_PROFILE_PATH.
* Mirrors handleUpdateAutoReview: optimistic update on the webview, rolled
* back via `issue/patch` if the persist fails.
*/
export async function handleUpdateProfilePath(panel: KanbanWebviewPanel, issueNumber: number, profilePath: string): Promise<void> {
const workspaceRoot = workspace.workspaceFolders?.[0]?.uri.fsPath
@@ -1661,12 +1665,94 @@ export async function handleUpdateProfilePath(panel: KanbanWebviewPanel, issueNu
}
}
/**
* Persist a per-issue `brainstormProfilePath` override into the issue's state
* JSON. Brainstorm start/resume use this profile (see resolveBrainstormProfilePath);
* empty = DEFAULT_PROFILE_PATH. 与实施/测试 profile 互不回退。
*/
export async function handleUpdateBrainstormProfilePath(panel: KanbanWebviewPanel, issueNumber: number, brainstormProfilePath: 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
}
let previousValue: string | undefined
try {
const existingState = await panel.readIssueState(issueNumber)
previousValue = typeof existingState.brainstormProfilePath === 'string'
? existingState.brainstormProfilePath
: undefined
}
catch {
previousValue = undefined
}
try {
await panel.mergeIssueState(issueNumber, { brainstormProfilePath })
logger.add({
level: 'info',
source: 'panel',
message: `工单 #${issueNumber} brainstormProfilePath=${brainstormProfilePath} 已持久化`,
})
}
catch (err) {
const message = err instanceof Error ? err.message : String(err)
logger.add({
level: 'error',
source: 'panel',
message: `持久化 brainstormProfilePath 失败 (issue #${issueNumber})`,
details: message,
})
panel.postMessage({
type: 'toast/show',
id: makeNonce(),
level: 'error',
message: `保存工单 #${issueNumber} 头脑风暴配置文件失败: ${message}`,
dismissOnTimer: 6000,
})
panel.postMessage({
type: 'issue/patch',
issueNumber,
patch: { brainstormProfilePath: previousValue },
})
}
}
/**
* Persist a per-issue `testProfilePath` override into the issue's state JSON
* comment. The manually-started test cc session launches with this profile
* (see resolveTestProfilePath); empty falls back to the implement `profilePath`,
* then DEFAULT_PROFILE_PATH. Mirrors handleUpdateProfilePath: optimistic update
* on the webview, rolled back via `issue/patch` if the persist fails.
* comment. Test start/resume use this profile (see resolveTestProfilePath);
* empty = DEFAULT_PROFILE_PATH,不回退实施 profile
*/
export async function handleUpdateTestProfilePath(panel: KanbanWebviewPanel, issueNumber: number, testProfilePath: string): Promise<void> {
const workspaceRoot = workspace.workspaceFolders?.[0]?.uri.fsPath
+33 -39
View File
@@ -68,12 +68,11 @@ export async function handleResumeSession(panel: KanbanWebviewPanel, sessionId:
existing.show(false)
return
}
// 实施 resume 走工单级 profilePath > 默认 的优先级(同 handleImplement);
// 头脑风暴 resume 保持现状,只看工单级 + 默认。
// 实施 / 头脑风暴各自独立:调用方传入对应字段,空 = DEFAULT。
const effectiveProfilePath
= sessionKind === 'implement'
? panel.resolveImplementProfilePath(profilePath)
: (profilePath && profilePath.trim() !== '' ? profilePath : DEFAULT_PROFILE_PATH)
: resolveBrainstormProfilePath(panel, profilePath)
// Reject paths containing single quotes — we shell-quote with single
// quotes below, and embedded quotes would break out of the wrap. In
// practice profile paths live under `/home/<user>/...` so this is a
@@ -977,14 +976,12 @@ export async function handleStartBrainstormSession(panel: KanbanWebviewPanel, is
return
}
// Pull profilePath from the issue's state JSON so the new cc session
// reuses whatever profile the user set when the issue was first created.
// Tolerated: missing/unparseable comment falls back to DEFAULT_PROFILE_PATH.
let profilePath: string | undefined
// 头脑风暴只用 brainstormProfilePath;空 = DEFAULT,不回退实施 profile。
let brainstormProfilePath: string | undefined
try {
const stateObj = await panel.readIssueState(issueNumber)
if (stateObj && typeof stateObj.profilePath === 'string' && stateObj.profilePath.length > 0)
profilePath = stateObj.profilePath
if (stateObj && typeof stateObj.brainstormProfilePath === 'string' && stateObj.brainstormProfilePath.length > 0)
brainstormProfilePath = stateObj.brainstormProfilePath
}
catch (err) {
logger.add({
@@ -995,11 +992,10 @@ export async function handleStartBrainstormSession(panel: KanbanWebviewPanel, is
})
}
const effectiveProfilePath
= profilePath && profilePath.trim() !== '' ? profilePath : DEFAULT_PROFILE_PATH
const effectiveProfilePath = resolveBrainstormProfilePath(panel, brainstormProfilePath)
if (effectiveProfilePath.includes('\'')) {
void window.showErrorMessage(
`启动规划失败:profilePath 含单引号,拒绝执行 (${effectiveProfilePath})`,
`启动规划失败:brainstormProfilePath 含单引号,拒绝执行 (${effectiveProfilePath})`,
)
return
}
@@ -1089,7 +1085,7 @@ export async function handleStartBrainstormSession(panel: KanbanWebviewPanel, is
* 与 handleStartBrainstormSession 的差异:
* - 首条 prompt 依赖 state JSON 里的 `pr`(合并的 PR 号);没有 PR 直接 toast 拒绝。
* - cwd 优先用 worktree(存在时),否则退回 workspaceRoot——测试本质要读真实代码,
* profile 走 resolveTestProfilePathtestProfilePath > profilePath > 默认)。
* profile 走 resolveTestProfilePathtestProfilePath > 默认,不回退实施)。
* - 捕获到的 session id 写进 state JSON 的 `testSessionId`。
*
* in-flight 锁 key `${issueNumber}:test`,防 createTerminal 期间用户重复点击。
@@ -1136,11 +1132,10 @@ export async function handleStartTestSession(panel: KanbanWebviewPanel, issueNum
return
}
// 从 state JSON 读 pr / profile / worktree。pr 仅在主 worktree(已合并)
// 从 state JSON 读 pr / testProfile / worktree。pr 仅在主 worktree(已合并)
// 测试时必需——那条路径要靠 tea 了解合并进来的改动;worktree 内测试代码
// 本就在工作目录,不依赖 PR。
let pr: string | undefined
let profilePath: string | undefined
let testProfilePath: string | undefined
let worktreePathRel: string | undefined
let branch: string | undefined
@@ -1149,8 +1144,6 @@ export async function handleStartTestSession(panel: KanbanWebviewPanel, issueNum
if (stateObj) {
if (typeof stateObj.pr === 'string' && stateObj.pr.length > 0)
pr = stateObj.pr
if (typeof stateObj.profilePath === 'string' && stateObj.profilePath.length > 0)
profilePath = stateObj.profilePath
if (typeof stateObj.testProfilePath === 'string' && stateObj.testProfilePath.length > 0)
testProfilePath = stateObj.testProfilePath
if (typeof stateObj.worktreePath === 'string' && stateObj.worktreePath.length > 0)
@@ -1179,11 +1172,11 @@ export async function handleStartTestSession(panel: KanbanWebviewPanel, issueNum
return
}
// 测试会话优先用专用 testProfilePath,未设置时回退实施 profile,再回退默认
const effectiveProfilePath = resolveTestProfilePath(panel, testProfilePath, profilePath)
// 测试会话用 testProfilePath;空 = DEFAULT,不回退实施 profile。
const effectiveProfilePath = resolveTestProfilePath(panel, testProfilePath)
if (effectiveProfilePath.includes('\'')) {
void window.showErrorMessage(
`启动测试失败:profilePath 含单引号,拒绝执行 (${effectiveProfilePath})`,
`启动测试失败:testProfilePath 含单引号,拒绝执行 (${effectiveProfilePath})`,
)
return
}
@@ -1289,7 +1282,7 @@ export async function handleStartTestSession(panel: KanbanWebviewPanel, issueNum
* `issue-${N}-测试`、命令 `claude ... --resume ${sessionId}`。
*
* cwd 优先用 worktreerelCwd 存在且在磁盘上),否则退回 workspaceRoot。
* profile 与 start 对称:resolveTestProfilePathtestProfilePath > profilePath > 默认)。
* profile 与 start 对称:resolveTestProfilePathtestProfilePath > 默认)。
* in-flight 锁 key `${issueNumber}:test`。
*/
export async function handleResumeTestSession(panel: KanbanWebviewPanel, sessionId: string, issueNumber: number, _relCwd?: string): Promise<void> {
@@ -1312,7 +1305,6 @@ export async function handleResumeTestSession(panel: KanbanWebviewPanel, session
let effectiveCwd = workspaceRoot
// 顺带读出 branch 给 impl-tab-pre-create 钩子用;读不到就空字符串。
let branchForHook = ''
let profilePath: string | undefined
let testProfilePath: string | undefined
if (workspaceRoot) {
const remote = await detectRepo(workspaceRoot)
@@ -1323,8 +1315,6 @@ export async function handleResumeTestSession(panel: KanbanWebviewPanel, session
const worktreePathRel = typeof stateObj?.worktreePath === 'string' ? stateObj.worktreePath : undefined
if (stateObj && typeof stateObj.branch === 'string')
branchForHook = stateObj.branch
if (typeof stateObj?.profilePath === 'string' && stateObj.profilePath.length > 0)
profilePath = stateObj.profilePath
if (typeof stateObj?.testProfilePath === 'string' && stateObj.testProfilePath.length > 0)
testProfilePath = stateObj.testProfilePath
effectiveCwd = await resolveTestSessionCwd(workspaceRoot, worktreePathRel)
@@ -1341,10 +1331,10 @@ export async function handleResumeTestSession(panel: KanbanWebviewPanel, session
}
// --settings 决定 provider/model/鉴权,resume 也必须与 start 一致解析 profile。
const effectiveProfilePath = resolveTestProfilePath(panel, testProfilePath, profilePath)
const effectiveProfilePath = resolveTestProfilePath(panel, testProfilePath)
if (effectiveProfilePath.includes('\'')) {
void window.showErrorMessage(
`resume 失败:profilePath 含单引号,拒绝执行 (${effectiveProfilePath})`,
`resume 失败:testProfilePath 含单引号,拒绝执行 (${effectiveProfilePath})`,
)
return
}
@@ -1611,15 +1601,12 @@ export async function startConflictResolution(panel: KanbanWebviewPanel, opts: {
* Pick the profile.json that implement-class cc sessions
* (handleImplement / handleResumeSession when sessionKind === 'implement')
* should launch with. Priority:
* 1. The per-issue `profilePath` recorded in state JSON (preserves the
* brainstorm-time choice, overridable from the issue detail panel).
* 2. The hard-coded `DEFAULT_PROFILE_PATH` fallback.
* 1. 工单级 `profilePath`(详情面板「实施配置文件」)
* 2. DEFAULT_PROFILE_PATH
*
* Conflict-resolution sessions use the global
* `settings.conflictResolutionProfilePath` instead of this helper.
* Brainstorm sessions deliberately don't go through this helper — they
* keep the legacy `profilePath || DEFAULT_PROFILE_PATH` so creators can
* still pick a profile per issue at brainstorm time.
* 冲突解决用全局 `settings.conflictResolutionProfilePath`。
* 头脑风暴用 resolveBrainstormProfilePath;测试用 resolveTestProfilePath。
* 三者互不回退。
*/
export function resolveImplementProfilePath(_panel: KanbanWebviewPanel, issueLevelProfilePath: string | undefined): string {
const issueLevel = issueLevelProfilePath?.trim()
@@ -1629,12 +1616,19 @@ export function resolveImplementProfilePath(_panel: KanbanWebviewPanel, issueLev
}
/**
* 测试会话 profile 解析:专用 `testProfilePath` 优先,未单独设置时回退到
* 实施会话的 `profilePath`,再回退默认。让测试会话既能独立锁 profile,
* 又默认与实施会话一致。
* 头脑风暴会话 profile:工单级 `brainstormProfilePath` > DEFAULT。
* 不回退实施 profile。
*/
export function resolveTestProfilePath(_panel: KanbanWebviewPanel, testProfilePath: string | undefined, implementProfilePath: string | undefined): string {
return testProfilePath?.trim() || implementProfilePath?.trim() || DEFAULT_PROFILE_PATH
export function resolveBrainstormProfilePath(_panel: KanbanWebviewPanel, brainstormProfilePath: string | undefined): string {
return brainstormProfilePath?.trim() || DEFAULT_PROFILE_PATH
}
/**
* 测试会话 profile:工单级 `testProfilePath` > DEFAULT。
* 不回退实施 profile。
*/
export function resolveTestProfilePath(_panel: KanbanWebviewPanel, testProfilePath: string | undefined): string {
return testProfilePath?.trim() || DEFAULT_PROFILE_PATH
}
/**
+1 -1
View File
@@ -346,7 +346,7 @@ export function handleCloseSessionTab(panel: KanbanWebviewPanel, issueNumber: nu
*/
export function takePendingIssueCreation(panel: KanbanWebviewPanel, nonce: string): {
sessionId?: string
profilePath?: string
brainstormProfilePath?: string
color: string
workspaceRoot: string
inboxDir: string
+3 -2
View File
@@ -42,7 +42,7 @@ export type ExtensionToWebview
= | { type: 'issues/loading' }
| { type: 'issues/update', issues: Issue[], globalAutoReview: boolean, youtrackConfigured: boolean }
| { type: 'issues/error', message: string }
| { type: 'issue/patch', issueNumber: number, patch: { autoReview?: boolean, specFile?: string, planFile?: string, prDiffFile?: string, sessionId?: string, implementSessionId?: string, reviewSessionId?: string, testSessionId?: string, pr?: string, implementStatus?: 'running' | 'done' | 'failed', column?: IssueColumn, worktreePath?: string, prMerged?: boolean, prMergedAt?: string, branch?: string, color?: string, worktreeExists?: boolean, brainstormTabOpen?: boolean, implementTabOpen?: boolean, reviewTabOpen?: boolean, testTabOpen?: boolean, profilePath?: string, testProfilePath?: string } }
| { type: 'issue/patch', issueNumber: number, patch: { autoReview?: boolean, specFile?: string, planFile?: string, prDiffFile?: string, sessionId?: string, implementSessionId?: string, reviewSessionId?: string, testSessionId?: string, pr?: string, implementStatus?: 'running' | 'done' | 'failed', column?: IssueColumn, worktreePath?: string, prMerged?: boolean, prMergedAt?: string, branch?: string, color?: string, worktreeExists?: boolean, brainstormTabOpen?: boolean, implementTabOpen?: boolean, reviewTabOpen?: boolean, testTabOpen?: boolean, profilePath?: string, brainstormProfilePath?: string, testProfilePath?: string } }
| { type: 'issue/pr-diff-summary-done', issueNumber: number }
| { type: 'issue/append', issue: Issue, select?: boolean }
| { type: 'issue/select-by-number', issueNumber: number }
@@ -141,7 +141,7 @@ export type WebviewToExtension
| { type: 'settings/edit-request' }
| { type: 'youtrack/list-projects', baseUrl: string, token: string }
| { type: 'youtrack/import' }
| { type: 'issue/create', userRequest: string, images?: Array<{ mediaType: string, base64: string }>, profilePath?: string }
| { type: 'issue/create', userRequest: string, images?: Array<{ mediaType: string, base64: string }>, brainstormProfilePath?: string }
| { type: 'toast/open-url', url: string }
| { type: 'session/resume', sessionId: string, profilePath?: string, cwd?: string, issueNumber?: number }
| { type: 'session/focus', issueNumber: number }
@@ -161,6 +161,7 @@ export type WebviewToExtension
| { type: 'dependency/clear', issueNumber: number, prerequisiteNumber: number }
| { type: 'issue/update-auto-review', issueNumber: number, value: boolean }
| { type: 'issue/update-profile-path', issueNumber: number, profilePath: string }
| { type: 'issue/update-brainstorm-profile-path', issueNumber: number, brainstormProfilePath: string }
| { type: 'issue/update-test-profile-path', issueNumber: number, testProfilePath: string }
| { type: 'logs/fetch' }
| { type: 'logs/clear' }