✨ feat(vscode): 审查会话链接按本机 codex 会话文件存在判定,不再绑 worktree
This commit is contained in:
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
"publisher": "clurdra",
|
"publisher": "clurdra",
|
||||||
"name": "superpowers-vscode-clurdra",
|
"name": "superpowers-vscode-clurdra",
|
||||||
"displayName": "Superpowers-clurdra",
|
"displayName": "Superpowers-clurdra",
|
||||||
"version": "0.2.81",
|
"version": "0.2.82",
|
||||||
"packageManager": "pnpm@10.27.0",
|
"packageManager": "pnpm@10.27.0",
|
||||||
"description": "Superpowers specs and plans Kanban explorer",
|
"description": "Superpowers specs and plans Kanban explorer",
|
||||||
"author": "clurdra",
|
"author": "clurdra",
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import { promises as fsp } from 'node:fs'
|
|||||||
import * as path from 'node:path'
|
import * as path from 'node:path'
|
||||||
|
|
||||||
/** UUID at the tail of `rollout-<ISO>-<uuid>.jsonl`. v7 UUIDs are still 8-4-4-4-12. */
|
/** UUID at the tail of `rollout-<ISO>-<uuid>.jsonl`. v7 UUIDs are still 8-4-4-4-12. */
|
||||||
const ROLLOUT_UUID_REGEX = /-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/i
|
export const ROLLOUT_UUID_REGEX = /-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/i
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Try to extract a thread_id (UUID) from a rollout filename.
|
* Try to extract a thread_id (UUID) from a rollout filename.
|
||||||
|
|||||||
@@ -68,6 +68,10 @@ export interface Issue {
|
|||||||
* the first time auto-review runs; reused for subsequent `synchronize`
|
* the first time auto-review runs; reused for subsequent `synchronize`
|
||||||
* webhook callbacks via `codex exec resume`. */
|
* webhook callbacks via `codex exec resume`. */
|
||||||
reviewSessionId?: string
|
reviewSessionId?: string
|
||||||
|
/** Whether the codex session file (`~/.codex/sessions/YYYY/MM/DD/rollout-...-<id>.jsonl`)
|
||||||
|
* for `reviewSessionId` exists on this machine; computed on load, never
|
||||||
|
* persisted. codex resume 只依赖这个文件,与 worktree 无关。 */
|
||||||
|
reviewSessionFileExists?: boolean
|
||||||
/** Session id of the test conversation (a Claude Code session id). Set the
|
/** Session id of the test conversation (a Claude Code session id). Set the
|
||||||
* first time the user manually starts a test session after the issue's PR
|
* first time the user manually starts a test session after the issue's PR
|
||||||
* is merged; reused for resume from the detail panel. */
|
* is merged; reused for resume from the detail panel. */
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { loadIssues } from '../gitea/issueLoader'
|
|||||||
import { overlayLocalIssueState } from '../issues/localState'
|
import { overlayLocalIssueState } from '../issues/localState'
|
||||||
import { closeIssueByRef, mergeIssueState, readIssueState } from '../issues/stateRouter'
|
import { closeIssueByRef, mergeIssueState, readIssueState } from '../issues/stateRouter'
|
||||||
import { logger } from '../logging/logger'
|
import { logger } from '../logging/logger'
|
||||||
|
import { annotateReviewSessionFileExists } from '../sessions/codexSessions'
|
||||||
import { getEffectiveCommitProfilePath, getSettings } from '../settings/store'
|
import { getEffectiveCommitProfilePath, getSettings } from '../settings/store'
|
||||||
import { webhookCoordinator } from '../webhook/coordinator'
|
import { webhookCoordinator } from '../webhook/coordinator'
|
||||||
import { loadYouTrackIssues } from '../youtrack/issueLoader'
|
import { loadYouTrackIssues } from '../youtrack/issueLoader'
|
||||||
@@ -899,9 +900,12 @@ export class KanbanWebviewPanel {
|
|||||||
dismissOnTimer: 6000,
|
dismissOnTimer: 6000,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// 本机记录(会话 id / worktree 等)叠加在共享状态之上,再补 live terminal 态。
|
// 本机记录(会话 id / worktree 等)叠加在共享状态之上,附加本机 codex
|
||||||
|
// 会话文件存在性,再补 live terminal 态。
|
||||||
const issues = this.withLiveTerminalTabState(
|
const issues = this.withLiveTerminalTabState(
|
||||||
overlayLocalIssueState(this.context, [...giteaIssues, ...youtrackList], workspaceRoot),
|
await annotateReviewSessionFileExists(
|
||||||
|
overlayLocalIssueState(this.context, [...giteaIssues, ...youtrackList], workspaceRoot),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
this.issueRefs = new Map(
|
this.issueRefs = new Map(
|
||||||
issues.map(i => [i.number, { source: i.source ?? 'gitea', externalId: i.externalId }] as const),
|
issues.map(i => [i.number, { source: i.source ?? 'gitea', externalId: i.externalId }] as const),
|
||||||
|
|||||||
@@ -238,8 +238,7 @@ export async function handleResumeReviewSession(panel: KanbanWebviewPanel, sessi
|
|||||||
try {
|
try {
|
||||||
// Server-side prerequisite gate — consistent with handleResumeSession /
|
// Server-side prerequisite gate — consistent with handleResumeSession /
|
||||||
// handleImplement. In practice review sessions imply the issue has
|
// handleImplement. In practice review sessions imply the issue has
|
||||||
// moved past todo (a worktree must exist), but enforcing here keeps
|
// moved past todo, but enforcing here keeps the contract uniform.
|
||||||
// the contract uniform.
|
|
||||||
const lockCheck = await panel.resolveLockedReason(issueNumber)
|
const lockCheck = await panel.resolveLockedReason(issueNumber)
|
||||||
if (lockCheck.locked) {
|
if (lockCheck.locked) {
|
||||||
logger.add({
|
logger.add({
|
||||||
@@ -257,15 +256,31 @@ export async function handleResumeReviewSession(panel: KanbanWebviewPanel, sessi
|
|||||||
existing.show(false)
|
existing.show(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// codex resume 只依赖 ~/.codex/sessions 下的 rollout 文件,与 worktree
|
||||||
|
// 无关:worktree 在则用它当 cwd,否则退回工作区根目录(与
|
||||||
|
// handleResumeSession 同款回退)。
|
||||||
const workspaceRoot = workspace.workspaceFolders?.[0]?.uri.fsPath
|
const workspaceRoot = workspace.workspaceFolders?.[0]?.uri.fsPath
|
||||||
if (!relCwd || !workspaceRoot) {
|
if (!workspaceRoot) {
|
||||||
void window.showErrorMessage(`审查会话无法恢复 #${issueNumber}:worktree 路径未记录`)
|
void window.showErrorMessage(`审查会话无法恢复 #${issueNumber}:未打开工作区`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const worktreeAbs = resolveWorktreePath(relCwd, workspaceRoot)
|
let effectiveCwd = workspaceRoot
|
||||||
if (!fs.existsSync(worktreeAbs)) {
|
let cwdFallback = true
|
||||||
void window.showErrorMessage(`审查会话无法恢复 #${issueNumber}:worktree 不存在 ${worktreeAbs}`)
|
if (relCwd) {
|
||||||
return
|
const worktreeAbs = resolveWorktreePath(relCwd, workspaceRoot)
|
||||||
|
if (fs.existsSync(worktreeAbs)) {
|
||||||
|
effectiveCwd = worktreeAbs
|
||||||
|
cwdFallback = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (cwdFallback) {
|
||||||
|
panel.postMessage({
|
||||||
|
type: 'toast/show',
|
||||||
|
id: makeNonce(),
|
||||||
|
level: 'info',
|
||||||
|
message: `工单 #${issueNumber} 的 worktree 不可用,审查会话将在工作区根目录恢复`,
|
||||||
|
dismissOnTimer: 5000,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
// Scan live terminals before creating a new one — survives panel
|
// Scan live terminals before creating a new one — survives panel
|
||||||
// reload / webview rebuild where `this.reviewTerminals` got wiped but
|
// reload / webview rebuild where `this.reviewTerminals` got wiped but
|
||||||
@@ -284,7 +299,7 @@ export async function handleResumeReviewSession(panel: KanbanWebviewPanel, sessi
|
|||||||
const { themeColor, iconUri } = await panel.resolveIssueIcon(issueNumber)
|
const { themeColor, iconUri } = await panel.resolveIssueIcon(issueNumber)
|
||||||
const terminal = window.createTerminal({
|
const terminal = window.createTerminal({
|
||||||
name: reviewTerminalName,
|
name: reviewTerminalName,
|
||||||
cwd: worktreeAbs,
|
cwd: effectiveCwd,
|
||||||
location: panel.resolveTerminalLocation(false),
|
location: panel.resolveTerminalLocation(false),
|
||||||
iconPath: iconUri,
|
iconPath: iconUri,
|
||||||
color: themeColor,
|
color: themeColor,
|
||||||
@@ -302,7 +317,7 @@ export async function handleResumeReviewSession(panel: KanbanWebviewPanel, sessi
|
|||||||
logger.add({
|
logger.add({
|
||||||
level: 'info',
|
level: 'info',
|
||||||
source: 'terminal',
|
source: 'terminal',
|
||||||
message: `已创建审查会话终端 #${issueNumber} cwd=${worktreeAbs}`,
|
message: `已创建审查会话终端 #${issueNumber} cwd=${effectiveCwd}`,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
@@ -590,10 +605,12 @@ async function triggerAutoReviewTabUnlocked(panel: KanbanWebviewPanel, opts: {
|
|||||||
if (!token)
|
if (!token)
|
||||||
return
|
return
|
||||||
await panel.mergeIssueState(opts.issueNumber, { reviewSessionId: threadId })
|
await panel.mergeIssueState(opts.issueNumber, { reviewSessionId: threadId })
|
||||||
|
// reviewSessionFileExists 是计算字段不持久化;rollout 文件刚在本机
|
||||||
|
// 落盘,patch 里直接置 true 让 resume 链接立即可点。
|
||||||
panel.postMessage({
|
panel.postMessage({
|
||||||
type: 'issue/patch',
|
type: 'issue/patch',
|
||||||
issueNumber: opts.issueNumber,
|
issueNumber: opts.issueNumber,
|
||||||
patch: { reviewSessionId: threadId },
|
patch: { reviewSessionId: threadId, reviewSessionFileExists: true },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export type ExtensionToWebview
|
|||||||
= | { type: 'issues/loading' }
|
= | { type: 'issues/loading' }
|
||||||
| { type: 'issues/update', issues: Issue[], scope: 'mine' | 'all', globalAutoReview: boolean, youtrackConfigured: boolean }
|
| { type: 'issues/update', issues: Issue[], scope: 'mine' | 'all', globalAutoReview: boolean, youtrackConfigured: boolean }
|
||||||
| { type: 'issues/error', message: string }
|
| { 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 | null, implementStatus?: 'running' | 'done' | 'failed', column?: IssueColumn, worktreePath?: string | null, prMerged?: boolean, prMergedAt?: string, branch?: string | null, color?: string, worktreeExists?: boolean, brainstormTabOpen?: boolean, implementTabOpen?: boolean, reviewTabOpen?: boolean, testTabOpen?: boolean, profilePath?: string, brainstormProfilePath?: string, testProfilePath?: string } }
|
| { type: 'issue/patch', issueNumber: number, patch: { autoReview?: boolean, specFile?: string, planFile?: string, prDiffFile?: string, sessionId?: string, implementSessionId?: string, reviewSessionId?: string, reviewSessionFileExists?: boolean, testSessionId?: string, pr?: string | null, implementStatus?: 'running' | 'done' | 'failed', column?: IssueColumn, worktreePath?: string | null, prMerged?: boolean, prMergedAt?: string, branch?: string | null, 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/pr-diff-summary-done', issueNumber: number }
|
||||||
| { type: 'issue/append', issue: Issue, select?: boolean }
|
| { type: 'issue/append', issue: Issue, select?: boolean }
|
||||||
| { type: 'issue/select-by-number', issueNumber: number }
|
| { type: 'issue/select-by-number', issueNumber: number }
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import type { Issue } from '../gitea/types'
|
||||||
|
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||||
|
import * as os from 'node:os'
|
||||||
|
import * as path from 'node:path'
|
||||||
|
import { afterEach, describe, expect, it } from 'vitest'
|
||||||
|
import { annotateReviewSessionFileExists, listCodexSessionIds } from './codexSessions'
|
||||||
|
|
||||||
|
const tempDirs: string[] = []
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
while (tempDirs.length > 0) {
|
||||||
|
const dir = tempDirs.pop()
|
||||||
|
if (dir)
|
||||||
|
rmSync(dir, { recursive: true, force: true })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function tempDir(prefix: string): string {
|
||||||
|
const dir = mkdtempSync(path.join(os.tmpdir(), prefix))
|
||||||
|
tempDirs.push(dir)
|
||||||
|
return dir
|
||||||
|
}
|
||||||
|
|
||||||
|
const UUID_A = '019dcb27-58a6-70a1-a5d1-bfc7f3ed9d0a'
|
||||||
|
const UUID_B = '11111111-2222-4333-8444-555555555555'
|
||||||
|
|
||||||
|
function writeRollout(baseDir: string, ymd: [string, string, string], uuid: string): void {
|
||||||
|
const dayDir = path.join(baseDir, ...ymd)
|
||||||
|
mkdirSync(dayDir, { recursive: true })
|
||||||
|
writeFileSync(path.join(dayDir, `rollout-2026-04-27T02-57-26-${uuid}.jsonl`), '{}\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('listCodexSessionIds', () => {
|
||||||
|
it('扫年/月/日三层目录,从文件名尾部提取全部会话 id', async () => {
|
||||||
|
const base = tempDir('codex-sessions-')
|
||||||
|
writeRollout(base, ['2026', '04', '27'], UUID_A)
|
||||||
|
writeRollout(base, ['2026', '05', '01'], UUID_B)
|
||||||
|
const ids = await listCodexSessionIds(base)
|
||||||
|
expect(ids).toEqual(new Set([UUID_A, UUID_B]))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('忽略不符合 rollout-*-<uuid>.jsonl 的文件', async () => {
|
||||||
|
const base = tempDir('codex-sessions-')
|
||||||
|
const dayDir = path.join(base, '2026', '04', '27')
|
||||||
|
mkdirSync(dayDir, { recursive: true })
|
||||||
|
writeFileSync(path.join(dayDir, 'notes.txt'), 'x')
|
||||||
|
writeFileSync(path.join(dayDir, 'rollout-2026-04-27T02-57-26-not-a-uuid.jsonl'), '{}\n')
|
||||||
|
writeRollout(base, ['2026', '04', '27'], UUID_A)
|
||||||
|
const ids = await listCodexSessionIds(base)
|
||||||
|
expect(ids).toEqual(new Set([UUID_A]))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('目录不存在返回空 Set', async () => {
|
||||||
|
const ids = await listCodexSessionIds(path.join(tempDir('codex-sessions-'), 'nope'))
|
||||||
|
expect(ids.size).toBe(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('annotateReviewSessionFileExists', () => {
|
||||||
|
const baseIssue = { number: 1, title: 't', column: 'review', htmlUrl: '' } as unknown as Issue
|
||||||
|
|
||||||
|
it('有会话文件的 issue 标 true,没有的标 false,无 reviewSessionId 的不加字段', async () => {
|
||||||
|
const base = tempDir('codex-sessions-')
|
||||||
|
writeRollout(base, ['2026', '04', '27'], UUID_A)
|
||||||
|
const issues: Issue[] = [
|
||||||
|
{ ...baseIssue, number: 1, reviewSessionId: UUID_A },
|
||||||
|
{ ...baseIssue, number: 2, reviewSessionId: UUID_B },
|
||||||
|
{ ...baseIssue, number: 3 },
|
||||||
|
]
|
||||||
|
const out = await annotateReviewSessionFileExists(issues, base)
|
||||||
|
expect(out[0].reviewSessionFileExists).toBe(true)
|
||||||
|
expect(out[1].reviewSessionFileExists).toBe(false)
|
||||||
|
expect('reviewSessionFileExists' in out[2]).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('id 大小写不影响判定', async () => {
|
||||||
|
const base = tempDir('codex-sessions-')
|
||||||
|
writeRollout(base, ['2026', '04', '27'], UUID_A)
|
||||||
|
const out = await annotateReviewSessionFileExists(
|
||||||
|
[{ ...baseIssue, reviewSessionId: UUID_A.toUpperCase() }],
|
||||||
|
base,
|
||||||
|
)
|
||||||
|
expect(out[0].reviewSessionFileExists).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
/**
|
||||||
|
* 枚举本机 codex 会话文件,判定「审查会话id」能否 resume。
|
||||||
|
*
|
||||||
|
* codex 把每个会话的 transcript 全局存在
|
||||||
|
* `~/.codex/sessions/YYYY/MM/DD/rollout-<ISO>-<uuid>.jsonl`,resume 只依赖
|
||||||
|
* 这个文件,与 worktree 无关——多人协作下 assignee 换机后,只要把会话文件
|
||||||
|
* 拷到目标机器就能 resume。
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Issue } from '../gitea/types'
|
||||||
|
import { promises as fsp } from 'node:fs'
|
||||||
|
import * as os from 'node:os'
|
||||||
|
import * as path from 'node:path'
|
||||||
|
import { ROLLOUT_UUID_REGEX } from '../cc/codexSessionWatcher'
|
||||||
|
|
||||||
|
/** codex 会话根目录(全局,不随工作区变化)。 */
|
||||||
|
export function defaultCodexSessionsDir(): string {
|
||||||
|
return path.join(os.homedir(), '.codex', 'sessions')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扫一遍 sessions 根目录(年/月/日三层),从文件名尾部提取所有会话 id
|
||||||
|
* 收进 Set(统一小写)。目录不存在或读取失败返回空 Set——调用方按
|
||||||
|
* 「本机没有会话文件」处理即可。
|
||||||
|
*/
|
||||||
|
export async function listCodexSessionIds(baseDir: string = defaultCodexSessionsDir()): Promise<Set<string>> {
|
||||||
|
const ids = new Set<string>()
|
||||||
|
let entries: string[]
|
||||||
|
try {
|
||||||
|
entries = await fsp.readdir(baseDir, { recursive: true })
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return ids
|
||||||
|
}
|
||||||
|
for (const entry of entries) {
|
||||||
|
const m = path.basename(entry).match(ROLLOUT_UUID_REGEX)
|
||||||
|
if (m)
|
||||||
|
ids.add(m[1].toLowerCase())
|
||||||
|
}
|
||||||
|
return ids
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 给带 reviewSessionId 的工单附加计算字段 `reviewSessionFileExists`
|
||||||
|
* (不落任何持久化)。整批工单只扫一次 sessions 目录;没有任何工单带
|
||||||
|
* id 时连扫都不扫。
|
||||||
|
*/
|
||||||
|
export async function annotateReviewSessionFileExists(issues: Issue[], baseDir?: string): Promise<Issue[]> {
|
||||||
|
if (!issues.some(i => typeof i.reviewSessionId === 'string' && i.reviewSessionId.length > 0))
|
||||||
|
return issues
|
||||||
|
const ids = await listCodexSessionIds(baseDir)
|
||||||
|
return issues.map(i => (i.reviewSessionId
|
||||||
|
? { ...i, reviewSessionFileExists: ids.has(i.reviewSessionId.toLowerCase()) }
|
||||||
|
: i))
|
||||||
|
}
|
||||||
@@ -24,6 +24,7 @@ import { mergeStateJsonComment, mergeStateJsonCommentGuarded, readStateJsonComme
|
|||||||
import { getLocalIssueState, mergeLocalIssueState, overlayLocalIssueState } from '../issues/localState'
|
import { getLocalIssueState, mergeLocalIssueState, overlayLocalIssueState } from '../issues/localState'
|
||||||
import { logger } from '../logging/logger'
|
import { logger } from '../logging/logger'
|
||||||
import { hasLiveIssueSessionTerminal } from '../panel/handlers/terminals'
|
import { hasLiveIssueSessionTerminal } from '../panel/handlers/terminals'
|
||||||
|
import { annotateReviewSessionFileExists } from '../sessions/codexSessions'
|
||||||
import { getSettings } from '../settings/store'
|
import { getSettings } from '../settings/store'
|
||||||
import { decideEventOwnership } from './eventOwnership'
|
import { decideEventOwnership } from './eventOwnership'
|
||||||
import { WebhookServer } from './server'
|
import { WebhookServer } from './server'
|
||||||
@@ -608,7 +609,7 @@ class WebhookCoordinator {
|
|||||||
workspaceRoot: ws,
|
workspaceRoot: ws,
|
||||||
issueNumber: event.issueNumber,
|
issueNumber: event.issueNumber,
|
||||||
})
|
})
|
||||||
const issue = loaded ? overlayLocalIssueState(this.ctx, [loaded], ws)[0] : null
|
const issue = loaded ? (await annotateReviewSessionFileExists(overlayLocalIssueState(this.ctx, [loaded], ws)))[0] : null
|
||||||
if (issue) {
|
if (issue) {
|
||||||
this.activePanel.postMessage({ type: 'issue/append', issue, select: pending ? true : undefined })
|
this.activePanel.postMessage({ type: 'issue/append', issue, select: pending ? true : undefined })
|
||||||
if (pending) {
|
if (pending) {
|
||||||
|
|||||||
@@ -254,12 +254,12 @@ export function IssueDetailPanel({
|
|||||||
onCloseSessionTab(issue.number, 'implement')
|
onCloseSessionTab(issue.number, 'implement')
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
issue?.worktreeExists
|
issue?.reviewSessionFileExists
|
||||||
? {
|
? {
|
||||||
key: 'reviewSessionId',
|
key: 'reviewSessionId',
|
||||||
label: '审查会话id',
|
label: '审查会话id',
|
||||||
type: 'action',
|
type: 'action',
|
||||||
description: '双击在新终端运行 codex resume <id> 查看审查会话(cwd 优先用 worktree)',
|
description: '双击在新终端运行 codex resume <id> 查看审查会话(worktree 在则用之,否则工作区根目录)',
|
||||||
actionIcon: <Terminal className="size-3.5" />,
|
actionIcon: <Terminal className="size-3.5" />,
|
||||||
onAction: (v) => {
|
onAction: (v) => {
|
||||||
if (typeof v !== 'string' || v.length === 0 || !issue)
|
if (typeof v !== 'string' || v.length === 0 || !issue)
|
||||||
@@ -282,7 +282,7 @@ export function IssueDetailPanel({
|
|||||||
label: '审查会话id',
|
label: '审查会话id',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
description: 'worktree 已清理,无法 resume;仅保留 id 文本',
|
description: '本机没有该 codex 会话文件,无法 resume;仅保留 id 文本',
|
||||||
},
|
},
|
||||||
(() => {
|
(() => {
|
||||||
// 测试会话 id 行有三种状态(照搬头脑风暴行的三态):
|
// 测试会话 id 行有三种状态(照搬头脑风暴行的三态):
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export type ExtensionToWebview
|
|||||||
= | { type: 'issues/loading' }
|
= | { type: 'issues/loading' }
|
||||||
| { type: 'issues/update', issues: Issue[], scope: 'mine' | 'all', globalAutoReview: boolean, youtrackConfigured: boolean }
|
| { type: 'issues/update', issues: Issue[], scope: 'mine' | 'all', globalAutoReview: boolean, youtrackConfigured: boolean }
|
||||||
| { type: 'issues/error', message: string }
|
| { 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 | null, implementStatus?: 'running' | 'done' | 'failed', column?: IssueColumn, worktreePath?: string | null, prMerged?: boolean, prMergedAt?: string, branch?: string | null, color?: string, worktreeExists?: boolean, brainstormTabOpen?: boolean, implementTabOpen?: boolean, reviewTabOpen?: boolean, testTabOpen?: boolean, profilePath?: string, brainstormProfilePath?: string, testProfilePath?: string } }
|
| { type: 'issue/patch', issueNumber: number, patch: { autoReview?: boolean, specFile?: string, planFile?: string, prDiffFile?: string, sessionId?: string, implementSessionId?: string, reviewSessionId?: string, reviewSessionFileExists?: boolean, testSessionId?: string, pr?: string | null, implementStatus?: 'running' | 'done' | 'failed', column?: IssueColumn, worktreePath?: string | null, prMerged?: boolean, prMergedAt?: string, branch?: string | null, 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/pr-diff-summary-done', issueNumber: number }
|
||||||
| { type: 'issue/append', issue: Issue, select?: boolean }
|
| { type: 'issue/append', issue: Issue, select?: boolean }
|
||||||
| { type: 'issue/select-by-number', issueNumber: number }
|
| { type: 'issue/select-by-number', issueNumber: number }
|
||||||
|
|||||||
@@ -51,6 +51,9 @@ export interface Issue {
|
|||||||
implementSessionId?: string
|
implementSessionId?: string
|
||||||
/** Backend-agnostic review session id (v1 stores a codex thread id). */
|
/** Backend-agnostic review session id (v1 stores a codex thread id). */
|
||||||
reviewSessionId?: string
|
reviewSessionId?: string
|
||||||
|
/** 本机是否存在 reviewSessionId 对应的 codex 会话文件;扩展端加载时计算,
|
||||||
|
* 不持久化。codex resume 只依赖这个文件,与 worktree 无关。 */
|
||||||
|
reviewSessionFileExists?: boolean
|
||||||
/** Claude Code session id of the test conversation; manually started after
|
/** Claude Code session id of the test conversation; manually started after
|
||||||
* the PR is merged, resumable from the detail panel. */
|
* the PR is merged, resumable from the detail panel. */
|
||||||
testSessionId?: string
|
testSessionId?: string
|
||||||
|
|||||||
Reference in New Issue
Block a user