✨ feat(vscode): 实施和审查会话行加启动箭头
不用再把卡片拖到「实施中」「审查」列: - 实施会话id 行在有计划文件且没实施会话时显示 ▶,等同拖进实施中列 - 审查会话id 行在有 PR 时显示 ▶,按 PR 事件那套流程起 codex 审查 Claude-Session: https://claude.ai/code/session_01Kgmb1LvrPmFX3NoUDB4zZC
This commit is contained in:
1 parent
620b7c73c8
commit
410a04da77
9 files changed
+101
-8
No files matched your search
@@ -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
|
||||||
|
|||||||
@@ -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'、终端名
|
||||||
|
|||||||
@@ -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' }
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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],
|
||||||
)
|
)
|
||||||
|
|
||||||
// 同事的工单只读:属性表统一消毒,顶部只留查看类按钮。
|
// 同事的工单只读:属性表统一消毒,顶部只留查看类按钮。
|
||||||
|
|||||||
@@ -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 }
|
||||||
}
|
}
|
||||||
@@ -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' }
|
||||||
|
|||||||
Reference in new issue
Block a user