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
+9 -4
View File
@@ -55,6 +55,7 @@ export function App() {
clearDependency,
updateIssueAutoReview,
updateIssueProfilePath,
updateIssueBrainstormProfilePath,
updateIssueTestProfilePath,
logs,
clearLogs,
@@ -90,9 +91,9 @@ export function App() {
// effect 跳过它——否则 选中→聚焦→终端激活→反向选中 会死循环、CPU 飙升。
const lastProgrammaticSelectRef = useRef<string | null>(null)
function handleSubmitNewIssue(userRequest: string, images: PastedImage[], profilePath?: string): void {
function handleSubmitNewIssue(userRequest: string, images: PastedImage[], brainstormProfilePath?: string): void {
const payload = images.map(({ mediaType, base64 }) => ({ mediaType, base64 }))
createIssue(userRequest, payload.length > 0 ? payload : undefined, profilePath)
createIssue(userRequest, payload.length > 0 ? payload : undefined, brainstormProfilePath)
setShowNewIssueModal(false)
}
@@ -195,7 +196,11 @@ export function App() {
e.preventDefault()
const isImpl = !!selectedIssue?.implementSessionId
const cwd = isImpl ? selectedIssue?.worktreePath : undefined
resumeSession(sid, selectedIssue?.profilePath, cwd, selectedIssue?.number)
// 实施 resume 用 profilePath;头脑风暴 resume 用 brainstormProfilePath。
const profileForResume = isImpl
? selectedIssue?.profilePath
: selectedIssue?.brainstormProfilePath
resumeSession(sid, profileForResume, cwd, selectedIssue?.number)
}
return
}
@@ -355,6 +360,7 @@ export function App() {
onStartBrainstormSession={startBrainstormSession}
onUpdateAutoReview={updateIssueAutoReview}
onUpdateProfilePath={updateIssueProfilePath}
onUpdateBrainstormProfilePath={updateIssueBrainstormProfilePath}
onUpdateTestProfilePath={updateIssueTestProfilePath}
onOpenLogs={() => setShowLogs(true)}
profileData={profileData}
@@ -424,7 +430,6 @@ export function App() {
onCancel={() => setShowNewIssueModal(false)}
onSubmit={handleSubmitNewIssue}
profiles={profiles}
defaultProfileName="offical"
/>
<LogModal
open={showLogs}
@@ -43,6 +43,7 @@ interface BottomTabsProps {
onStartBrainstormSession: (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
onOpenLogs: () => void
@@ -120,6 +121,7 @@ export function BottomTabs(props: BottomTabsProps) {
onStartBrainstormSession={props.onStartBrainstormSession}
onUpdateAutoReview={props.onUpdateAutoReview}
onUpdateProfilePath={props.onUpdateProfilePath}
onUpdateBrainstormProfilePath={props.onUpdateBrainstormProfilePath}
onUpdateTestProfilePath={props.onUpdateTestProfilePath}
profiles={props.profiles}
onOpenLogs={props.onOpenLogs}
@@ -61,6 +61,8 @@ interface IssueDetailPanelProps {
profiles: ClaudeProfile[]
/** Persist a per-issue `profilePath` override into the state JSON. */
onUpdateProfilePath: (issueNumber: number, profilePath: string) => void
/** Persist a per-issue `brainstormProfilePath` override into the state JSON. */
onUpdateBrainstormProfilePath: (issueNumber: number, brainstormProfilePath: string) => void
/** Persist a per-issue `testProfilePath` override into the state JSON. */
onUpdateTestProfilePath: (issueNumber: number, testProfilePath: string) => void
/** Open the in-webview log modal. */
@@ -99,6 +101,7 @@ export function IssueDetailPanel({
onUpdateAutoReview,
profiles,
onUpdateProfilePath,
onUpdateBrainstormProfilePath,
onUpdateTestProfilePath,
onOpenLogs,
}: IssueDetailPanelProps) {
@@ -195,7 +198,7 @@ export function IssueDetailPanel({
return
if (!throttleBySessionId(v))
return
onResumeSession(v, issue?.profilePath, undefined, issue?.number)
onResumeSession(v, issue?.brainstormProfilePath, undefined, issue?.number)
},
secondaryActionIcon,
secondaryActionTitle,
@@ -321,40 +324,46 @@ export function IssueDetailPanel({
description: '首次开会话时随机分配的终端颜色(VS Code ThemeColor),用于该工单所有会话的终端 tab 着色',
},
{
key: 'profilePath',
label: '实施配置文件',
key: 'brainstormProfilePath',
label: '头脑风暴配置文件',
type: 'select',
options: (() => {
// 占位项:value='' 对应「未设置」。原生 select 在 value 不匹配任何
// option 时会假显示第一项且 selectedIndex=0,导致点第一项不触发
// onChange、永远存不进去;占位项让未设置态有真实匹配项。
const opts = [{ label: '(默认)', value: '' }, ...profiles.map(p => ({ label: p.name, value: p.path }))]
const current = issue?.profilePath
// 自定义路径兜底:state JSON 里存的 profile 不在已知列表时,
// 额外追加一项以免下拉显示空白、丢失当前选择。
const current = issue?.brainstormProfilePath
if (current && !opts.some(o => o.value === current))
opts.push({ label: `自定义:${current}`, value: current })
return opts
})(),
description: '实施会话使用的 Claude 配置文件;选「(默认)」用内置默认(offical),选具体 profile 则覆盖(持久化 state JSON',
description: '头脑风暴会话使用的 Claude 配置文件;选「(默认)」用内置默认,与实施/测试互不回退',
},
{
key: 'profilePath',
label: '实施配置文件',
type: 'select',
options: (() => {
const opts = [{ label: '(默认)', value: '' }, ...profiles.map(p => ({ label: p.name, value: p.path }))]
const current = issue?.profilePath
if (current && !opts.some(o => o.value === current))
opts.push({ label: `自定义:${current}`, value: current })
return opts
})(),
description: '实施会话使用的 Claude 配置文件;选「(默认)」用内置默认,与头脑风暴/测试互不回退',
},
{
key: 'testProfilePath',
label: '测试配置文件',
type: 'select',
options: (() => {
// 占位项:value='' 对应「未设置」。原生 select 在 value 不匹配任何
// option 时会假显示第一项且 selectedIndex=0,导致点第一项不触发
// onChange、永远存不进去;占位项让未设置态有真实匹配项。
const opts = [{ label: '(默认)', value: '' }, ...profiles.map(p => ({ label: p.name, value: p.path }))]
const current = issue?.testProfilePath
// 自定义路径兜底:state JSON 里存的 profile 不在已知列表时,
// 额外追加一项以免下拉显示空白、丢失当前选择。
if (current && !opts.some(o => o.value === current))
opts.push({ label: `自定义:${current}`, value: current })
return opts
})(),
description: '测试会话使用的 Claude 配置文件;留空时回退实施配置文件',
description: '测试会话使用的 Claude 配置文件;选「(默认)」用内置默认,与头脑风暴/实施互不回退',
},
{
key: 'specFile',
@@ -476,6 +485,7 @@ export function IssueDetailPanel({
testSessionId: issue.testSessionId ?? null,
autoReview: issue.autoReview ?? globalAutoReview,
color: issue.color ?? null,
brainstormProfilePath: issue.brainstormProfilePath ?? null,
profilePath: issue.profilePath ?? null,
testProfilePath: issue.testProfilePath ?? null,
specFile: issue.specFile ?? null,
@@ -582,6 +592,8 @@ export function IssueDetailPanel({
onChange={(key, value) => {
if (key === 'autoReview' && issue && typeof value === 'boolean')
onUpdateAutoReview(issue.number, value)
if (key === 'brainstormProfilePath' && issue && typeof value === 'string')
onUpdateBrainstormProfilePath(issue.number, value)
if (key === 'profilePath' && issue && typeof value === 'string')
onUpdateProfilePath(issue.number, value)
if (key === 'testProfilePath' && issue && typeof value === 'string')
@@ -33,7 +33,7 @@ export interface ClaudeProfile {
interface Props {
open: boolean
onCancel: () => void
onSubmit: (userRequest: string, images: PastedImage[], profilePath?: string) => void
onSubmit: (userRequest: string, images: PastedImage[], brainstormProfilePath?: string) => void
profiles: ClaudeProfile[]
defaultProfileName?: string
}
@@ -55,13 +55,10 @@ export function NewIssueModal({ open, onCancel, onSubmit, profiles, defaultProfi
setSelectedProfile(null)
return
}
// Pick default profile when the modal opens. Prefer the one matching
// `defaultProfileName`, otherwise fall back to the first alphabetically.
if (profiles.length > 0) {
const match = defaultProfileName
? profiles.find(p => p.name === defaultProfileName)
: undefined
setSelectedProfile(match ? match.name : profiles[0].name)
// 默认不预选 profile(空 = 系统默认);仅当显式传入 defaultProfileName 时预选。
if (profiles.length > 0 && defaultProfileName) {
const match = profiles.find(p => p.name === defaultProfileName)
setSelectedProfile(match ? match.name : null)
}
else {
setSelectedProfile(null)
@@ -208,7 +205,7 @@ export function NewIssueModal({ open, onCancel, onSubmit, profiles, defaultProfi
function handleSubmit(): void {
if (!canSubmit)
return
const profilePath = selectedProfile
const brainstormProfilePath = selectedProfile
? profiles.find(p => p.name === selectedProfile)?.path
: undefined
const pending = [...pastedTexts]
@@ -224,7 +221,7 @@ export function NewIssueModal({ open, onCancel, onSubmit, profiles, defaultProfi
onSubmit(
finalRequest,
images.map(({ mediaType, base64, previewDataUrl }) => ({ mediaType, base64, previewDataUrl })),
profilePath,
brainstormProfilePath,
)
}
@@ -248,8 +245,19 @@ export function NewIssueModal({ open, onCancel, onSubmit, profiles, defaultProfi
{profiles.length > 0 && (
<fieldset className="mb-3 border-0 p-0">
<legend className="mb-1 text-xs opacity-70"></legend>
<legend className="mb-1 text-xs opacity-70"></legend>
<div className="flex flex-wrap gap-3 text-xs">
<label className="flex cursor-pointer items-center gap-1">
<input
type="radio"
name="claude-profile"
value=""
checked={selectedProfile === null}
onChange={() => setSelectedProfile(null)}
className="accent-[var(--vscode-focusBorder)]"
/>
<span></span>
</label>
{profiles.map(p => (
<label key={p.name} className="flex cursor-pointer items-center gap-1">
<input
+23 -6
View File
@@ -127,7 +127,7 @@ export interface UseIssuesResult {
youtrackProjects: YouTrackProjectsState
dismissSettings: () => void
requestEditAuth: () => void
createIssue: (userRequest: string, images?: Array<{ mediaType: string, base64: string }>, profilePath?: string) => void
createIssue: (userRequest: string, images?: Array<{ mediaType: string, base64: string }>, brainstormProfilePath?: string) => void
dismissToast: (id: string) => void
openUrl: (url: string) => void
resumeSession: (sessionId: string, profilePath?: string, cwd?: string, issueNumber?: number) => void
@@ -153,8 +153,8 @@ export interface UseIssuesResult {
* onDidCloseTerminal and clears `*TabOpen` so the X button disappears. */
closeSessionTab: (issueNumber: number, kind: 'brainstorm' | 'implement' | 'review' | 'test') => void
/** Spawn a fresh "规划" cc tab for an existing issue whose sessionId is
* still empty. The extension picks up profilePath from the issue's state
* JSON, watches `~/.claude/projects/<encoded-workspaceRoot>` for the new
* still empty. Extension 用工单级 brainstormProfilePath(空 = 默认),
* watches `~/.claude/projects/<encoded-workspaceRoot>` for the new
* session jsonl, then writes the id back as `sessionId`. */
startBrainstormSession: (issueNumber: number) => void
changeColumn: (issueNumber: number, toColumn: IssueColumn, source?: 'gitea' | 'youtrack', externalId?: string) => void
@@ -164,6 +164,9 @@ export interface UseIssuesResult {
/** 覆盖该工单实施会话使用的 Claude 配置文件,乐观更新本地 state 后
* postMessage 持久化到 state JSON。失败时扩展端 push `issue/patch` 回滚。 */
updateIssueProfilePath: (issueNumber: number, profilePath: string) => void
/** 覆盖该工单头脑风暴会话专用的 Claude 配置文件,乐观更新本地 state 后
* postMessage 持久化到 state JSON。失败时扩展端 push `issue/patch` 回滚。 */
updateIssueBrainstormProfilePath: (issueNumber: number, brainstormProfilePath: string) => void
/** 覆盖该工单测试会话专用的 Claude 配置文件,乐观更新本地 state 后
* postMessage 持久化到 state JSON。失败时扩展端 push `issue/patch` 回滚。 */
updateIssueTestProfilePath: (issueNumber: number, testProfilePath: string) => void
@@ -314,9 +317,9 @@ export function useIssues(): UseIssuesResult {
const createIssue = useCallback((
userRequest: string,
images?: Array<{ mediaType: string, base64: string }>,
profilePath?: string,
brainstormProfilePath?: string,
): void => {
postMessage({ type: 'issue/create', userRequest, images, profilePath })
postMessage({ type: 'issue/create', userRequest, images, brainstormProfilePath })
}, [])
const dismissToast = useCallback((id: string): void => {
@@ -458,6 +461,20 @@ export function useIssues(): UseIssuesResult {
postMessage({ type: 'issue/update-profile-path', issueNumber, profilePath })
}, [])
const updateIssueBrainstormProfilePath = useCallback((issueNumber: number, brainstormProfilePath: string): void => {
setState((prev) => {
if (prev.status !== 'ready')
return prev
return {
status: 'ready',
issues: prev.issues.map(i =>
i.number === issueNumber ? { ...i, brainstormProfilePath } : i,
),
}
})
postMessage({ type: 'issue/update-brainstorm-profile-path', issueNumber, brainstormProfilePath })
}, [])
const updateIssueTestProfilePath = useCallback((issueNumber: number, testProfilePath: string): void => {
// Optimistically update local state so the dropdown reflects immediately
// without remounting the issues list / detail panel via a full reload.
@@ -721,5 +738,5 @@ export function useIssues(): UseIssuesResult {
return cleanup
}, [clearPrDiffSummaryRunning])
return { state, settings, globalAutoReview, youtrackConfigured, importYouTrack, toasts, profiles, setIssues, refresh, saveSettings, listYouTrackProjects, youtrackProjects, dismissSettings, requestEditAuth, createIssue, dismissToast, openUrl, resumeSession, resumeReviewSession, startTestSession, resumeTestSession, focusSession, openFile, implement, generatePrDiffSummary, isPrDiffSummaryRunning, openPr, openWorktree, deleteWorktree, mergeBranch, deleteIssue, closeIssue, closeSessionTab, startBrainstormSession, changeColumn, setDependency, clearDependency, updateIssueAutoReview, updateIssueProfilePath, updateIssueTestProfilePath, logs, fetchLogs, clearLogs, pendingSelectId, clearPendingSelect, commitRunning, runCommit, hasChanges, branchSyncBehind, branchSyncRunning, branchSyncDisabled, branchSyncTitle, runBranchSync, envLocked, envFileCount, envLockRunning, envLockTitle, toggleEnvLock }
return { state, settings, globalAutoReview, youtrackConfigured, importYouTrack, toasts, profiles, setIssues, refresh, saveSettings, listYouTrackProjects, youtrackProjects, dismissSettings, requestEditAuth, createIssue, dismissToast, openUrl, resumeSession, resumeReviewSession, startTestSession, resumeTestSession, focusSession, openFile, implement, generatePrDiffSummary, isPrDiffSummaryRunning, openPr, openWorktree, deleteWorktree, mergeBranch, deleteIssue, closeIssue, closeSessionTab, startBrainstormSession, changeColumn, setDependency, clearDependency, updateIssueAutoReview, updateIssueProfilePath, updateIssueBrainstormProfilePath, updateIssueTestProfilePath, logs, fetchLogs, clearLogs, pendingSelectId, clearPendingSelect, commitRunning, runCommit, hasChanges, branchSyncBehind, branchSyncRunning, branchSyncDisabled, branchSyncTitle, runBranchSync, envLocked, envFileCount, envLockRunning, envLockTitle, toggleEnvLock }
}
+3 -2
View File
@@ -57,7 +57,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 }
@@ -156,7 +156,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 }
@@ -176,6 +176,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' }
+4 -2
View File
@@ -16,9 +16,11 @@ export interface Issue {
sessionId?: string
/** Absolute path to the Claude settings profile used at creation; passed
* as --settings on resume. */
/** 实施会话专用的 Claude 配置文件;空 = 默认,不回退其它字段。 */
profilePath?: string
/** 测试会话专用的 Claude 配置文件,独立于实施会话的 `profilePath`
* 留空时测试会话回退到 `profilePath`,再回退默认。 */
/** 头脑风暴会话专用的 Claude 配置文件;空 = 默认,不回退其它字段。 */
brainstormProfilePath?: string
/** 测试会话专用的 Claude 配置文件;空 = 默认,不回退其它字段。 */
testProfilePath?: string
/** Optional workspace-relative path to the spec file for this issue, as
* surfaced from the Claude session transcript. Lives under