From 410a04da77f3df6a766a55c9ee6f4cd2655a1b99 Mon Sep 17 00:00:00 2001 From: cruldra Date: Sun, 13 Sep 2026 10:52:14 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(vscode):=20=E5=AE=9E=E6=96=BD?= =?UTF-8?q?=E5=92=8C=E5=AE=A1=E6=9F=A5=E4=BC=9A=E8=AF=9D=E8=A1=8C=E5=8A=A0?= =?UTF-8?q?=E5=90=AF=E5=8A=A8=E7=AE=AD=E5=A4=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 不用再把卡片拖到「实施中」「审查」列: - 实施会话id 行在有计划文件且没实施会话时显示 ▶,等同拖进实施中列 - 审查会话id 行在有 PR 时显示 ▶,按 PR 事件那套流程起 codex 审查 Claude-Session: https://claude.ai/code/session_01Kgmb1LvrPmFX3NoUDB4zZC --- vscode/src/panel/KanbanPanel.ts | 4 ++ vscode/src/panel/handlers/sessions.ts | 53 ++++++++++++++++++- vscode/src/panel/messages.ts | 1 + vscode/src/panel/readOnlyGuard.ts | 1 + vscode/webview-ui/src/App.tsx | 2 + .../webview-ui/src/components/BottomTabs.tsx | 2 + .../src/components/IssueDetailPanel.tsx | 38 ++++++++++--- vscode/webview-ui/src/hooks/useIssues.ts | 7 ++- vscode/webview-ui/src/lib/messages.ts | 1 + 9 files changed, 101 insertions(+), 8 deletions(-) diff --git a/vscode/src/panel/KanbanPanel.ts b/vscode/src/panel/KanbanPanel.ts index a906191..b2524a9 100644 --- a/vscode/src/panel/KanbanPanel.ts +++ b/vscode/src/panel/KanbanPanel.ts @@ -439,6 +439,10 @@ export class KanbanWebviewPanel { void sessions.handleResumeReviewSession(this, msg.sessionId, msg.issueNumber, msg.cwd) return } + if (msg.type === 'session/start-review') { + void sessions.handleStartReviewSession(this, msg.issueNumber) + return + } if (msg.type === 'session/start-test') { void sessions.handleStartTestSession(this, msg.issueNumber) return diff --git a/vscode/src/panel/handlers/sessions.ts b/vscode/src/panel/handlers/sessions.ts index dc1cb49..ccf3574 100644 --- a/vscode/src/panel/handlers/sessions.ts +++ b/vscode/src/panel/handlers/sessions.ts @@ -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 } from '../../cc/prompts' +import { getBrainstormContinuePrompt, getImplementPlanPrompt, getReviewPrompt } from '../../cc/prompts' import { projectsDirFor, watchForNewSession } from '../../cc/sessionWatcher' import { detectRepo } from '../../git/remote' 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 { + 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 时) * 触发,照搬 handleResumeSession 的 implement 分支,但 kind='test'、终端名 diff --git a/vscode/src/panel/messages.ts b/vscode/src/panel/messages.ts index 428abb5..9baf1b8 100644 --- a/vscode/src/panel/messages.ts +++ b/vscode/src/panel/messages.ts @@ -193,6 +193,7 @@ 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/start-review', issueNumber: number } | { type: 'session/start-test', issueNumber: number } | { type: 'editor/open-file', path: string } | { type: 'profiles/list' } diff --git a/vscode/src/panel/readOnlyGuard.ts b/vscode/src/panel/readOnlyGuard.ts index d11c185..d728b65 100644 --- a/vscode/src/panel/readOnlyGuard.ts +++ b/vscode/src/panel/readOnlyGuard.ts @@ -14,6 +14,7 @@ export const OWNER_ONLY_MESSAGE_TYPES: ReadonlySet = new Set([ 'session/focus', 'session/resume-review', 'session/resume-test', + 'session/start-review', 'session/start-test', 'session/close-tab', 'brainstorm/continue', diff --git a/vscode/webview-ui/src/App.tsx b/vscode/webview-ui/src/App.tsx index 8c93299..512f9c7 100644 --- a/vscode/webview-ui/src/App.tsx +++ b/vscode/webview-ui/src/App.tsx @@ -41,6 +41,7 @@ export function App() { openUrl, resumeSession, isSessionTabPending, + startReviewSession, resumeReviewSession, startTestSession, resumeTestSession, @@ -375,6 +376,7 @@ export function App() { onOpenInBrowser={openUrl} onResumeSession={resumeSession} isSessionTabPending={isSessionTabPending} + onStartReviewSession={startReviewSession} onResumeReviewSession={resumeReviewSession} onStartTestSession={startTestSession} onResumeTestSession={resumeTestSession} diff --git a/vscode/webview-ui/src/components/BottomTabs.tsx b/vscode/webview-ui/src/components/BottomTabs.tsx index 60d1e6c..f045c45 100644 --- a/vscode/webview-ui/src/components/BottomTabs.tsx +++ b/vscode/webview-ui/src/components/BottomTabs.tsx @@ -31,6 +31,7 @@ interface BottomTabsProps { onResumeSession: (sessionId: string, kind: 'implement' | 'brainstorm', profilePath?: string, cwd?: string, issueNumber?: number) => void isSessionTabPending: (sessionId: string) => boolean onResumeReviewSession: (sessionId: string, issueNumber: number, cwd?: string) => void + onStartReviewSession: (issueNumber: number) => void onStartTestSession: (issueNumber: number) => void onResumeTestSession: (sessionId: string, issueNumber: number, cwd?: string) => void onOpenFile: (path: string) => void @@ -130,6 +131,7 @@ export function BottomTabs(props: BottomTabsProps) { onResumeSession={props.onResumeSession} isSessionTabPending={props.isSessionTabPending} onResumeReviewSession={props.onResumeReviewSession} + onStartReviewSession={props.onStartReviewSession} onStartTestSession={props.onStartTestSession} onResumeTestSession={props.onResumeTestSession} onOpenFile={props.onOpenFile} diff --git a/vscode/webview-ui/src/components/IssueDetailPanel.tsx b/vscode/webview-ui/src/components/IssueDetailPanel.tsx index 6878d52..2e73ff6 100644 --- a/vscode/webview-ui/src/components/IssueDetailPanel.tsx +++ b/vscode/webview-ui/src/components/IssueDetailPanel.tsx @@ -55,6 +55,7 @@ interface IssueDetailPanelProps { * Spawn a fresh "测试" cc tab for an issue whose PR has merged. Bound to the * Play button next to the "测试会话 id" row (only shown when `issue.pr` exists). */ + onStartReviewSession: (issueNumber: number) => void onStartTestSession: (issueNumber: number) => void /** Open a workspace-relative file in the editor. */ onOpenFile: (path: string) => void @@ -133,6 +134,7 @@ export function IssueDetailPanel({ onResumeSession, onResumeReviewSession, onResumeTestSession, + onStartReviewSession, onStartTestSession, onOpenFile, onImplement, @@ -290,13 +292,27 @@ export function IssueDetailPanel({ return onResumeSession(v, 'implement', issue?.profilePath, issue?.worktreePath, issue?.number) }, - secondaryActions: issue?.implementTabOpen - ? [{ + secondaryActions: (() => { + if (issue?.implementTabOpen) { + return [{ icon: , title: '关闭实施 tab', onClick: () => onCloseSessionTab(issue.number, 'implement'), }] - : [], + } + // 还没实施会话时给个 ▶,省去把卡片拖进「实施中」列。 + if (issue?.implementSessionId || !issue?.planFile) + return [] + return [{ + icon: , + title: locked ? lockTitle : '启动实施会话', + disabled: + issue.implementStatus === 'running' + || issue.implementStatus === 'done' + || locked, + onClick: () => onImplement(issue.number, issue.planFile ?? '', issue.profilePath, issue.sessionId), + }] + })(), }, issue?.reviewSessionFileExists ? { @@ -324,9 +340,19 @@ export function IssueDetailPanel({ : { key: 'reviewSessionId', label: '审查会话id', - type: 'string', + type: 'action', readOnly: true, - description: '本机没有该 codex 会话文件,无法 resume;仅保留 id 文本', + description: issue?.reviewSessionId + ? '本机没有该 codex 会话文件,无法 resume;点 ▶ 对当前 PR 重新起一个审查会话' + : '还没有审查会话;点 ▶ 对当前 PR 启动 codex 审查(等同 PR 事件自动触发的那次)', + secondaryActions: issue?.pr + ? [{ + icon: , + title: locked ? lockTitle : '启动审查会话', + disabled: locked, + onClick: () => onStartReviewSession(issue.number), + }] + : [], }, (() => { // 测试会话 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], ) // 同事的工单只读:属性表统一消毒,顶部只留查看类按钮。 diff --git a/vscode/webview-ui/src/hooks/useIssues.ts b/vscode/webview-ui/src/hooks/useIssues.ts index 44fdda8..6aa59e8 100644 --- a/vscode/webview-ui/src/hooks/useIssues.ts +++ b/vscode/webview-ui/src/hooks/useIssues.ts @@ -187,6 +187,7 @@ export interface UseIssuesResult { /** 该会话 id 的开 tab 请求还没被扩展处理完 → 行内转 spinner。 */ isSessionTabPending: (sessionId: string) => boolean resumeReviewSession: (sessionId: string, issueNumber: number, cwd?: string) => void + startReviewSession: (issueNumber: number) => void startTestSession: (issueNumber: number) => void resumeTestSession: (sessionId: string, issueNumber: number, cwd?: string) => void focusSession: (issueNumber: number) => void @@ -545,6 +546,10 @@ export function useIssues(): UseIssuesResult { postMessage({ type: 'session/resume-review', sessionId, issueNumber, cwd }) }, [markSessionTabPending]) + const startReviewSession = useCallback((issueNumber: number): void => { + postMessage({ type: 'session/start-review', issueNumber }) + }, []) + const startTestSession = useCallback((issueNumber: number): void => { postMessage({ type: 'session/start-test', issueNumber }) }, []) @@ -990,5 +995,5 @@ export function useIssues(): UseIssuesResult { return cleanup }, [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 } } diff --git a/vscode/webview-ui/src/lib/messages.ts b/vscode/webview-ui/src/lib/messages.ts index 36b46de..9b2d14d 100644 --- a/vscode/webview-ui/src/lib/messages.ts +++ b/vscode/webview-ui/src/lib/messages.ts @@ -224,6 +224,7 @@ 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/start-review', issueNumber: number } | { type: 'session/start-test', issueNumber: number } | { type: 'editor/open-file', path: string } | { type: 'profiles/list' }