✨ feat(vscode): 实施和审查会话行加启动箭头

不用再把卡片拖到「实施中」「审查」列:

- 实施会话id 行在有计划文件且没实施会话时显示 ▶,等同拖进实施中列
- 审查会话id 行在有 PR 时显示 ▶,按 PR 事件那套流程起 codex 审查

Claude-Session: https://claude.ai/code/session_01Kgmb1LvrPmFX3NoUDB4zZC
This commit is contained in:
cruldra committed 2026-09-13 10:52:14 +08:00
1 parent 620b7c73c8
commit 410a04da77
9 files changed
+101 -8

No files matched your search

+4
View File
@@ -439,6 +439,10 @@ export class KanbanWebviewPanel {
void sessions.handleResumeReviewSession(this, msg.sessionId, msg.issueNumber, msg.cwd) void sessions.handleResumeReviewSession(this, msg.sessionId, msg.issueNumber, msg.cwd)
return return
} }
if (msg.type === 'session/start-review') {
void sessions.handleStartReviewSession(this, msg.issueNumber)
return
}
if (msg.type === 'session/start-test') { if (msg.type === 'session/start-test') {
void sessions.handleStartTestSession(this, msg.issueNumber) void sessions.handleStartTestSession(this, msg.issueNumber)
return return
+52 -1
View File
@@ -13,7 +13,7 @@ import { buildCcCommand } from '../../cc/ccCommand'
import { buildCodexCommandString } from '../../cc/codexCommand' import { buildCodexCommandString } from '../../cc/codexCommand'
import { watchForNewCodexSession } from '../../cc/codexSessionWatcher' import { watchForNewCodexSession } from '../../cc/codexSessionWatcher'
import { resolveProfilePath } from '../../cc/profiles' import { resolveProfilePath } from '../../cc/profiles'
import { getBrainstormContinuePrompt, getImplementPlanPrompt } from '../../cc/prompts' import { getBrainstormContinuePrompt, getImplementPlanPrompt, getReviewPrompt } from '../../cc/prompts'
import { projectsDirFor, watchForNewSession } from '../../cc/sessionWatcher' import { projectsDirFor, watchForNewSession } from '../../cc/sessionWatcher'
import { detectRepo } from '../../git/remote' import { detectRepo } from '../../git/remote'
import { ensureWorktree, resolveWorktreePath } from '../../git/worktree' import { ensureWorktree, resolveWorktreePath } from '../../git/worktree'
@@ -1270,6 +1270,57 @@ export async function handleStartTestSession(panel: KanbanWebviewPanel, issueNum
} }
} }
/**
* 手动启动审查会话:等同 PR 事件触发的自动审查,只是入口在详情面板的
* 「审查会话id」行。必须已有 PR——审查提示词就是让 codex 去看这个 PR。
*/
export async function handleStartReviewSession(panel: KanbanWebviewPanel, issueNumber: number): Promise<void> {
const workspaceRoot = workspace.workspaceFolders?.[0]?.uri.fsPath
if (!workspaceRoot) {
void window.showErrorMessage('请先打开一个工作区文件夹')
return
}
const lockCheck = await panel.resolveLockedReason(issueNumber)
if (lockCheck.locked) {
await window.showWarningMessage(`等待前置工单 #${lockCheck.prerequisiteNumber} 完成`)
return
}
let pr: string | undefined
let worktreePath = ''
try {
const stateObj = await panel.readIssueState(issueNumber)
if (typeof stateObj.pr === 'string' && stateObj.pr.length > 0)
pr = stateObj.pr.replace(/\(已合并\)\s*$/, '')
if (typeof stateObj.worktreePath === 'string')
worktreePath = stateObj.worktreePath
}
catch (err) {
logger.add({
level: 'warn',
source: 'review',
message: `读取 #${issueNumber} state JSON 失败,无法启动审查会话`,
details: err instanceof Error ? err.message : String(err),
})
}
if (!pr) {
void window.showWarningMessage('该工单还没有 PR,无法启动审查会话')
return
}
const ok = await triggerAutoReviewTab(panel, {
issueNumber,
prNumber: pr,
prompt: getReviewPrompt(panel.context, { prNumber: pr }),
worktreePath,
workspaceRoot,
})
if (!ok)
void window.showErrorMessage(`启动审查会话失败 #${issueNumber},详见扩展日志`)
}
/** /**
* 恢复一个已存在的测试 cc 会话:详情面板点测试会话行(testTabOpen=false 时) * 恢复一个已存在的测试 cc 会话:详情面板点测试会话行(testTabOpen=false 时)
* 触发,照搬 handleResumeSession 的 implement 分支,但 kind='test'、终端名 * 触发,照搬 handleResumeSession 的 implement 分支,但 kind='test'、终端名
+1
View File
@@ -193,6 +193,7 @@ export type WebviewToExtension
| { type: 'session/focus', issueNumber: number } | { type: 'session/focus', issueNumber: number }
| { type: 'session/resume-review', sessionId: string, issueNumber: number, cwd?: string } | { type: 'session/resume-review', sessionId: string, issueNumber: number, cwd?: string }
| { type: 'session/resume-test', sessionId: string, issueNumber: number, cwd?: string } | { type: 'session/resume-test', sessionId: string, issueNumber: number, cwd?: string }
| { type: 'session/start-review', issueNumber: number }
| { type: 'session/start-test', issueNumber: number } | { type: 'session/start-test', issueNumber: number }
| { type: 'editor/open-file', path: string } | { type: 'editor/open-file', path: string }
| { type: 'profiles/list' } | { type: 'profiles/list' }
+1
View File
@@ -14,6 +14,7 @@ export const OWNER_ONLY_MESSAGE_TYPES: ReadonlySet<string> = new Set([
'session/focus', 'session/focus',
'session/resume-review', 'session/resume-review',
'session/resume-test', 'session/resume-test',
'session/start-review',
'session/start-test', 'session/start-test',
'session/close-tab', 'session/close-tab',
'brainstorm/continue', 'brainstorm/continue',
+2
View File
@@ -41,6 +41,7 @@ export function App() {
openUrl, openUrl,
resumeSession, resumeSession,
isSessionTabPending, isSessionTabPending,
startReviewSession,
resumeReviewSession, resumeReviewSession,
startTestSession, startTestSession,
resumeTestSession, resumeTestSession,
@@ -375,6 +376,7 @@ export function App() {
onOpenInBrowser={openUrl} onOpenInBrowser={openUrl}
onResumeSession={resumeSession} onResumeSession={resumeSession}
isSessionTabPending={isSessionTabPending} isSessionTabPending={isSessionTabPending}
onStartReviewSession={startReviewSession}
onResumeReviewSession={resumeReviewSession} onResumeReviewSession={resumeReviewSession}
onStartTestSession={startTestSession} onStartTestSession={startTestSession}
onResumeTestSession={resumeTestSession} onResumeTestSession={resumeTestSession}
@@ -31,6 +31,7 @@ interface BottomTabsProps {
onResumeSession: (sessionId: string, kind: 'implement' | 'brainstorm', profilePath?: string, cwd?: string, issueNumber?: number) => void onResumeSession: (sessionId: string, kind: 'implement' | 'brainstorm', profilePath?: string, cwd?: string, issueNumber?: number) => void
isSessionTabPending: (sessionId: string) => boolean isSessionTabPending: (sessionId: string) => boolean
onResumeReviewSession: (sessionId: string, issueNumber: number, cwd?: string) => void onResumeReviewSession: (sessionId: string, issueNumber: number, cwd?: string) => void
onStartReviewSession: (issueNumber: number) => void
onStartTestSession: (issueNumber: number) => void onStartTestSession: (issueNumber: number) => void
onResumeTestSession: (sessionId: string, issueNumber: number, cwd?: string) => void onResumeTestSession: (sessionId: string, issueNumber: number, cwd?: string) => void
onOpenFile: (path: string) => void onOpenFile: (path: string) => void
@@ -130,6 +131,7 @@ export function BottomTabs(props: BottomTabsProps) {
onResumeSession={props.onResumeSession} onResumeSession={props.onResumeSession}
isSessionTabPending={props.isSessionTabPending} isSessionTabPending={props.isSessionTabPending}
onResumeReviewSession={props.onResumeReviewSession} onResumeReviewSession={props.onResumeReviewSession}
onStartReviewSession={props.onStartReviewSession}
onStartTestSession={props.onStartTestSession} onStartTestSession={props.onStartTestSession}
onResumeTestSession={props.onResumeTestSession} onResumeTestSession={props.onResumeTestSession}
onOpenFile={props.onOpenFile} onOpenFile={props.onOpenFile}
@@ -55,6 +55,7 @@ interface IssueDetailPanelProps {
* Spawn a fresh "测试" cc tab for an issue whose PR has merged. Bound to the * Spawn a fresh "测试" cc tab for an issue whose PR has merged. Bound to the
* Play button next to the "测试会话 id" row (only shown when `issue.pr` exists). * Play button next to the "测试会话 id" row (only shown when `issue.pr` exists).
*/ */
onStartReviewSession: (issueNumber: number) => void
onStartTestSession: (issueNumber: number) => void onStartTestSession: (issueNumber: number) => void
/** Open a workspace-relative file in the editor. */ /** Open a workspace-relative file in the editor. */
onOpenFile: (path: string) => void onOpenFile: (path: string) => void
@@ -133,6 +134,7 @@ export function IssueDetailPanel({
onResumeSession, onResumeSession,
onResumeReviewSession, onResumeReviewSession,
onResumeTestSession, onResumeTestSession,
onStartReviewSession,
onStartTestSession, onStartTestSession,
onOpenFile, onOpenFile,
onImplement, onImplement,
@@ -290,13 +292,27 @@ export function IssueDetailPanel({
return return
onResumeSession(v, 'implement', issue?.profilePath, issue?.worktreePath, issue?.number) onResumeSession(v, 'implement', issue?.profilePath, issue?.worktreePath, issue?.number)
}, },
secondaryActions: issue?.implementTabOpen secondaryActions: (() => {
? [{ if (issue?.implementTabOpen) {
return [{
icon: <X className="size-3.5" />, icon: <X className="size-3.5" />,
title: '关闭实施 tab', title: '关闭实施 tab',
onClick: () => onCloseSessionTab(issue.number, 'implement'), onClick: () => onCloseSessionTab(issue.number, 'implement'),
}] }]
: [], }
// 还没实施会话时给个 ▶,省去把卡片拖进「实施中」列。
if (issue?.implementSessionId || !issue?.planFile)
return []
return [{
icon: <Play className="size-3.5" />,
title: locked ? lockTitle : '启动实施会话',
disabled:
issue.implementStatus === 'running'
|| issue.implementStatus === 'done'
|| locked,
onClick: () => onImplement(issue.number, issue.planFile ?? '', issue.profilePath, issue.sessionId),
}]
})(),
}, },
issue?.reviewSessionFileExists issue?.reviewSessionFileExists
? { ? {
@@ -324,9 +340,19 @@ export function IssueDetailPanel({
: { : {
key: 'reviewSessionId', key: 'reviewSessionId',
label: '审查会话id', label: '审查会话id',
type: 'string', type: 'action',
readOnly: true, readOnly: true,
description: '本机没有该 codex 会话文件,无法 resume;仅保留 id 文本', description: issue?.reviewSessionId
? '本机没有该 codex 会话文件,无法 resume;点 ▶ 对当前 PR 重新起一个审查会话'
: '还没有审查会话;点 ▶ 对当前 PR 启动 codex 审查(等同 PR 事件自动触发的那次)',
secondaryActions: issue?.pr
? [{
icon: <Play className="size-3.5" />,
title: locked ? lockTitle : '启动审查会话',
disabled: locked,
onClick: () => onStartReviewSession(issue.number),
}]
: [],
}, },
(() => { (() => {
// 测试会话 id 行有三种状态(照搬头脑风暴行的三态): // 测试会话 id 行有三种状态(照搬头脑风暴行的三态):
@@ -542,7 +568,7 @@ export function IssueDetailPanel({
], ],
}, },
], ],
[onResumeSession, onResumeReviewSession, onResumeTestSession, onStartTestSession, onOpenFile, onImplement, onOpenPr, onGeneratePrDiffSummary, onOpenWorktree, onDeleteWorktree, onMergeBranch, onCloseSessionTab, onContinueBrainstorm, onClearBrainstormSession, onOpenLogs, issue, locked, lockTitle, globalAutoReview, prDiffSummaryRunning, profiles, isSessionTabPending], [onResumeSession, onResumeReviewSession, onResumeTestSession, onStartTestSession, onOpenFile, onImplement, onOpenPr, onGeneratePrDiffSummary, onOpenWorktree, onDeleteWorktree, onMergeBranch, onCloseSessionTab, onContinueBrainstorm, onClearBrainstormSession, onOpenLogs, issue, locked, lockTitle, globalAutoReview, prDiffSummaryRunning, profiles, isSessionTabPending, onStartReviewSession],
) )
// 同事的工单只读:属性表统一消毒,顶部只留查看类按钮。 // 同事的工单只读:属性表统一消毒,顶部只留查看类按钮。
+6 -1
View File
@@ -187,6 +187,7 @@ export interface UseIssuesResult {
/** 该会话 id 的开 tab 请求还没被扩展处理完 → 行内转 spinner。 */ /** 该会话 id 的开 tab 请求还没被扩展处理完 → 行内转 spinner。 */
isSessionTabPending: (sessionId: string) => boolean isSessionTabPending: (sessionId: string) => boolean
resumeReviewSession: (sessionId: string, issueNumber: number, cwd?: string) => void resumeReviewSession: (sessionId: string, issueNumber: number, cwd?: string) => void
startReviewSession: (issueNumber: number) => void
startTestSession: (issueNumber: number) => void startTestSession: (issueNumber: number) => void
resumeTestSession: (sessionId: string, issueNumber: number, cwd?: string) => void resumeTestSession: (sessionId: string, issueNumber: number, cwd?: string) => void
focusSession: (issueNumber: number) => void focusSession: (issueNumber: number) => void
@@ -545,6 +546,10 @@ export function useIssues(): UseIssuesResult {
postMessage({ type: 'session/resume-review', sessionId, issueNumber, cwd }) postMessage({ type: 'session/resume-review', sessionId, issueNumber, cwd })
}, [markSessionTabPending]) }, [markSessionTabPending])
const startReviewSession = useCallback((issueNumber: number): void => {
postMessage({ type: 'session/start-review', issueNumber })
}, [])
const startTestSession = useCallback((issueNumber: number): void => { const startTestSession = useCallback((issueNumber: number): void => {
postMessage({ type: 'session/start-test', issueNumber }) postMessage({ type: 'session/start-test', issueNumber })
}, []) }, [])
@@ -990,5 +995,5 @@ export function useIssues(): UseIssuesResult {
return cleanup return cleanup
}, [clearPrDiffSummaryRunning, markHandoffRunning]) }, [clearPrDiffSummaryRunning, markHandoffRunning])
return { state, settings, avatars, isSessionTabPending, globalAutoReview, youtrackConfigured, importYouTrack, toasts, profiles, setIssues, refresh, refreshing, scope, toggleScope, saveSettings, listYouTrackProjects, youtrackProjects, dismissSettings, requestEditAuth, createIssue, dismissToast, openUrl, resumeSession, resumeReviewSession, startTestSession, resumeTestSession, focusSession, openFile, implement, generatePrDiffSummary, isPrDiffSummaryRunning, openPr, openWorktree, deleteWorktree, mergeBranch, deleteIssue, closeIssue, closeSessionTab, continueBrainstorm, changeColumn, setDependency, clearDependency, updateIssueAutoReview, updateIssueProfilePath, updateIssueBrainstormProfilePath, clearBrainstormSession, updateIssueTestProfilePath, logs, fetchLogs, clearLogs, pendingSelectId, clearPendingSelect, commitRunning, runCommit, hasChanges, release, runRelease, cherryPick, dismissCherryPick, confirmCherryPick, envLocked, envFileCount, envLockRunning, envLockTitle, toggleEnvLock, me, handoffUsers, requestHandoffUsers, startHandoff, acceptHandoff, isHandoffRunning } return { state, settings, avatars, isSessionTabPending, globalAutoReview, youtrackConfigured, importYouTrack, toasts, profiles, setIssues, refresh, refreshing, scope, toggleScope, saveSettings, listYouTrackProjects, youtrackProjects, dismissSettings, requestEditAuth, createIssue, dismissToast, openUrl, resumeSession, resumeReviewSession, startReviewSession, startTestSession, resumeTestSession, focusSession, openFile, implement, generatePrDiffSummary, isPrDiffSummaryRunning, openPr, openWorktree, deleteWorktree, mergeBranch, deleteIssue, closeIssue, closeSessionTab, continueBrainstorm, changeColumn, setDependency, clearDependency, updateIssueAutoReview, updateIssueProfilePath, updateIssueBrainstormProfilePath, clearBrainstormSession, updateIssueTestProfilePath, logs, fetchLogs, clearLogs, pendingSelectId, clearPendingSelect, commitRunning, runCommit, hasChanges, release, runRelease, cherryPick, dismissCherryPick, confirmCherryPick, envLocked, envFileCount, envLockRunning, envLockTitle, toggleEnvLock, me, handoffUsers, requestHandoffUsers, startHandoff, acceptHandoff, isHandoffRunning }
} }
+1
View File
@@ -224,6 +224,7 @@ export type WebviewToExtension
| { type: 'session/focus', issueNumber: number } | { type: 'session/focus', issueNumber: number }
| { type: 'session/resume-review', sessionId: string, issueNumber: number, cwd?: string } | { type: 'session/resume-review', sessionId: string, issueNumber: number, cwd?: string }
| { type: 'session/resume-test', sessionId: string, issueNumber: number, cwd?: string } | { type: 'session/resume-test', sessionId: string, issueNumber: number, cwd?: string }
| { type: 'session/start-review', issueNumber: number }
| { type: 'session/start-test', issueNumber: number } | { type: 'session/start-test', issueNumber: number }
| { type: 'editor/open-file', path: string } | { type: 'editor/open-file', path: string }
| { type: 'profiles/list' } | { type: 'profiles/list' }