🐛 fix(vscode): 别的机器上开的会话在本机没有会话文件时,会话 id 行置灰不可双击,不再 resume 报错
This commit is contained in:
1 parent
2b1e660cc8
commit
bf4661e364
20 files changed
+508
-234
No files matched your search
@@ -153,12 +153,14 @@ describe('payloads', () => {
|
||||
profiles: {},
|
||||
worktreePath: '/wt',
|
||||
worktreeExists: true,
|
||||
missingSessionIds: [],
|
||||
})).toEqual({
|
||||
handoffAttachmentId: null,
|
||||
handoffFrom: null,
|
||||
sessionId: 's',
|
||||
worktreePath: '/wt',
|
||||
worktreeExists: true,
|
||||
missingSessionIds: [],
|
||||
})
|
||||
})
|
||||
it('接管 UI patch:无 worktreePath 时显式置 null,不留旧值', () => {
|
||||
@@ -166,35 +168,30 @@ describe('payloads', () => {
|
||||
sessions: { sessionId: 's' },
|
||||
profiles: {},
|
||||
worktreeExists: false,
|
||||
missingSessionIds: [],
|
||||
})).toEqual({
|
||||
handoffAttachmentId: null,
|
||||
handoffFrom: null,
|
||||
sessionId: 's',
|
||||
worktreePath: null,
|
||||
worktreeExists: false,
|
||||
missingSessionIds: [],
|
||||
})
|
||||
})
|
||||
it('接管 UI patch:带 reviewSessionFileExists 时一并下发,UI 不用等下次刷新才能点链接', () => {
|
||||
it('接管 UI patch:缺失的会话 id 原样下发,UI 立刻知道哪几行不能 resume', () => {
|
||||
expect(handoffAcceptedUiPatch({
|
||||
sessions: { reviewSessionId: 'r' },
|
||||
sessions: { sessionId: 's', reviewSessionId: 'r' },
|
||||
profiles: {},
|
||||
worktreeExists: false,
|
||||
reviewSessionFileExists: true,
|
||||
missingSessionIds: ['r'],
|
||||
})).toEqual({
|
||||
handoffAttachmentId: null,
|
||||
handoffFrom: null,
|
||||
sessionId: 's',
|
||||
reviewSessionId: 'r',
|
||||
worktreePath: null,
|
||||
worktreeExists: false,
|
||||
reviewSessionFileExists: true,
|
||||
missingSessionIds: ['r'],
|
||||
})
|
||||
})
|
||||
it('接管 UI patch:reviewSessionFileExists 未传时不出现在 patch 里', () => {
|
||||
const patch = handoffAcceptedUiPatch({
|
||||
sessions: { sessionId: 's' },
|
||||
profiles: {},
|
||||
worktreeExists: false,
|
||||
})
|
||||
expect(patch).not.toHaveProperty('reviewSessionFileExists')
|
||||
})
|
||||
})
|
||||
@@ -110,7 +110,7 @@ export function handoffAcceptedUiPatch(local: {
|
||||
profiles: HandoffProfiles
|
||||
worktreePath?: string
|
||||
worktreeExists: boolean
|
||||
reviewSessionFileExists?: boolean
|
||||
missingSessionIds: string[]
|
||||
}): Record<string, unknown> {
|
||||
return {
|
||||
handoffAttachmentId: null,
|
||||
@@ -121,6 +121,7 @@ export function handoffAcceptedUiPatch(local: {
|
||||
// 与 worktreeExists: false 对不上。
|
||||
worktreePath: local.worktreePath ?? null,
|
||||
worktreeExists: local.worktreeExists,
|
||||
...(local.reviewSessionFileExists !== undefined ? { reviewSessionFileExists: local.reviewSessionFileExists } : {}),
|
||||
// 同理空数组也要发:webview 里留着接管前(会话文件还没装进本机)算的缺失列表。
|
||||
missingSessionIds: local.missingSessionIds,
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
uploadIssueAttachment,
|
||||
} from '../../gitea/api'
|
||||
import { logger } from '../../logging/logger'
|
||||
import { defaultCodexSessionsDir } from '../../sessions/codexSessions'
|
||||
import { annotateMissingSessionIds, defaultCodexSessionsDir } from '../../sessions/sessionFiles'
|
||||
import { getSettings } from '../../settings/store'
|
||||
import { makeNonce } from '../KanbanPanel'
|
||||
import { cleanupFeatureBranch } from './branchCleanup'
|
||||
@@ -464,11 +464,9 @@ export async function handleHandoffAccept(panel: KanbanWebviewPanel, issueNumber
|
||||
})
|
||||
|
||||
// ④ 本机字段 + 清共享字段;⑤ 删附件(失败只 warn)
|
||||
// reviewSessionFileExists 是计算字段,board 只在启动时算一次(annotateReviewSessionFileExists);
|
||||
// 接管刚把 codex 会话文件装进本机,这里顺手算好一起下发,UI 不用等下次刷新才能点「审查会话」链接。
|
||||
const reviewSessionId = manifest.sessions.reviewSessionId
|
||||
const reviewSessionFileExists = !!reviewSessionId
|
||||
&& manifest.codex.some(c => c.id.toLowerCase() === reviewSessionId.toLowerCase())
|
||||
// missingSessionIds 是计算字段,board 只在加载时算;会话文件刚装进本机,
|
||||
// 这里按真实文件重算一起下发,UI 不用等下次刷新就能双击 resume。
|
||||
const [{ missingSessionIds = [] }] = await annotateMissingSessionIds([manifest.sessions])
|
||||
const local = { sessions: manifest.sessions, profiles: manifest.profiles, worktreePath: worktreeAbs }
|
||||
await panel.mergeIssueState(issueNumber, handoffAcceptedStateExtra(local))
|
||||
try {
|
||||
@@ -482,7 +480,7 @@ export async function handleHandoffAccept(panel: KanbanWebviewPanel, issueNumber
|
||||
panel.postMessage({
|
||||
type: 'issue/patch',
|
||||
issueNumber,
|
||||
patch: handoffAcceptedUiPatch({ ...local, worktreeExists: !!worktreeAbs, reviewSessionFileExists }),
|
||||
patch: handoffAcceptedUiPatch({ ...local, worktreeExists: !!worktreeAbs, missingSessionIds }),
|
||||
})
|
||||
toast(panel, 'success', `已接管 #${issueNumber}(来自 ${manifest.from})${worktreeAbs ? `,worktree:${worktreeAbs}` : ''}`)
|
||||
logger.add({ level: 'info', source: 'panel', message: `接管 #${issueNumber} 完成`, details: JSON.stringify({ manifest, worktreeAbs, pathRewrite }) })
|
||||
|
||||
@@ -4,7 +4,6 @@ import type { ResumeKind } from './resumeTarget'
|
||||
import { execFile } from 'node:child_process'
|
||||
import * as fs from 'node:fs'
|
||||
import { promises as fsp } from 'node:fs'
|
||||
import * as os from 'node:os'
|
||||
import * as path from 'node:path'
|
||||
import { window, workspace } from 'vscode'
|
||||
import { loginForToken } from '../../auth/identity'
|
||||
@@ -20,6 +19,7 @@ import { ensureWorktree, resolveWorktreePath } from '../../git/worktree'
|
||||
import { DEFAULT_WORKTREE_TEMPLATE, deriveSlug, resolveWorktreeDir } from '../../git/worktreePath'
|
||||
import { updateIssueAssignees } from '../../gitea/api'
|
||||
import { logger } from '../../logging/logger'
|
||||
import { claudeSessionFileExists, codexSessionFileExists, defaultCodexSessionsDir } from '../../sessions/sessionFiles'
|
||||
import { getSettings } from '../../settings/store'
|
||||
import { webhookCoordinator } from '../../webhook/coordinator'
|
||||
import { decideAutoReviewPath } from '../autoReviewDecision'
|
||||
@@ -57,6 +57,26 @@ function nonEmptyString(value: unknown): string | undefined {
|
||||
return typeof value === 'string' && value.length > 0 ? value : undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* 会话 id 来自 Gitea 共享状态,别的机器上开的会话本机没有会话文件,resume
|
||||
* 必然报「No conversation found」。各 resume 入口在开新终端前查一次,没有就
|
||||
* 用这条 toast 拦下(已开着的 tab 直接复用,不受影响)。
|
||||
*/
|
||||
function rejectMissingSessionFile(panel: KanbanWebviewPanel, sessionId: string, issueNumber?: number): void {
|
||||
logger.add({
|
||||
level: 'warn',
|
||||
source: 'panel',
|
||||
message: `拒绝 resume #${issueNumber ?? '?'}:本机没有会话 ${sessionId} 的会话文件`,
|
||||
})
|
||||
panel.postMessage({
|
||||
type: 'toast/show',
|
||||
id: makeNonce(),
|
||||
level: 'error',
|
||||
message: `本机没有会话 ${sessionId} 的会话文件(可能在别的机器上创建),无法 resume`,
|
||||
dismissOnTimer: 8000,
|
||||
})
|
||||
}
|
||||
|
||||
export async function handleResumeSession(panel: KanbanWebviewPanel, sessionId: string, sessionKind: ResumeKind, profilePath?: string, relCwd?: string, issueNumber?: number): Promise<void> {
|
||||
// In-flight 锁:防 impl-tab-pre-create 钩子 await(pgrep pycharm 最多 15s)
|
||||
// 期间用户重复点同一会话 id 触发重复 createTerminal。issueNumber 为空时
|
||||
@@ -162,6 +182,10 @@ export async function handleResumeSession(panel: KanbanWebviewPanel, sessionId:
|
||||
existingByName.show(false)
|
||||
return
|
||||
}
|
||||
if (!(await claudeSessionFileExists(sessionId))) {
|
||||
rejectMissingSessionFile(panel, sessionId, issueNumber)
|
||||
return
|
||||
}
|
||||
// 实施 resume 走新建 terminal 路径且 worktree 还在时触发 impl-tab-pre-create
|
||||
// 钩子(启动 pycharm 等)。existingByName fast path 不调——pycharm 应该已
|
||||
// 在跑,重复启会冲突;worktree 没了也不调——钩子拿 WORKTREE_PATH 开
|
||||
@@ -263,6 +287,21 @@ export async function handleResumeReviewSession(panel: KanbanWebviewPanel, sessi
|
||||
existing.show(false)
|
||||
return
|
||||
}
|
||||
// Scan live terminals before creating a new one — survives panel
|
||||
// reload / webview rebuild where `this.reviewTerminals` got wiped but
|
||||
// the tab is still open.
|
||||
const reviewTerminalName = `issue-${issueNumber}-审查`
|
||||
const existingByName = panel.findExistingTerminal(reviewTerminalName)
|
||||
if (existingByName) {
|
||||
panel.reviewTerminals.set(sessionId, existingByName)
|
||||
panel.trackSessionTerminal(existingByName, issueNumber, 'review')
|
||||
existingByName.show(false)
|
||||
return
|
||||
}
|
||||
if (!(await codexSessionFileExists(sessionId))) {
|
||||
rejectMissingSessionFile(panel, sessionId, issueNumber)
|
||||
return
|
||||
}
|
||||
// codex resume 只依赖 ~/.codex/sessions 下的 rollout 文件,与 worktree
|
||||
// 无关:worktree 在则用它当 cwd,否则退回工作区根目录(与
|
||||
// handleResumeSession 同款回退)。
|
||||
@@ -289,17 +328,6 @@ export async function handleResumeReviewSession(panel: KanbanWebviewPanel, sessi
|
||||
dismissOnTimer: 5000,
|
||||
})
|
||||
}
|
||||
// Scan live terminals before creating a new one — survives panel
|
||||
// reload / webview rebuild where `this.reviewTerminals` got wiped but
|
||||
// the tab is still open.
|
||||
const reviewTerminalName = `issue-${issueNumber}-审查`
|
||||
const existingByName = panel.findExistingTerminal(reviewTerminalName)
|
||||
if (existingByName) {
|
||||
panel.reviewTerminals.set(sessionId, existingByName)
|
||||
panel.trackSessionTerminal(existingByName, issueNumber, 'review')
|
||||
existingByName.show(false)
|
||||
return
|
||||
}
|
||||
// VS Code's default editor-tab icon (`>` arrow) does NOT honor the
|
||||
// `color` option. Force a `circle-filled` codicon so the tab actually
|
||||
// shows the issue color; keep `color` too for panel-view fallback.
|
||||
@@ -547,9 +575,8 @@ async function triggerAutoReviewTabUnlocked(panel: KanbanWebviewPanel, opts: {
|
||||
// ~/.codex/sessions/YYYY/MM/DD/ on every run; we extract thread_id from
|
||||
// its name and persist it as reviewSessionId so the detail panel's
|
||||
// resume button works after the user closes this tab.
|
||||
const codexSessionsRoot = path.join(os.homedir(), '.codex', 'sessions')
|
||||
const watcherPromise = watchForNewCodexSession({
|
||||
baseDir: codexSessionsRoot,
|
||||
baseDir: defaultCodexSessionsDir(),
|
||||
timeoutMs: 120_000,
|
||||
})
|
||||
|
||||
@@ -599,12 +626,10 @@ async function triggerAutoReviewTabUnlocked(panel: KanbanWebviewPanel, opts: {
|
||||
if (!token)
|
||||
return
|
||||
await panel.mergeIssueState(opts.issueNumber, { reviewSessionId: threadId })
|
||||
// reviewSessionFileExists 是计算字段不持久化;rollout 文件刚在本机
|
||||
// 落盘,patch 里直接置 true 让 resume 链接立即可点。
|
||||
panel.postMessage({
|
||||
type: 'issue/patch',
|
||||
issueNumber: opts.issueNumber,
|
||||
patch: { reviewSessionId: threadId, reviewSessionFileExists: true },
|
||||
patch: { reviewSessionId: threadId },
|
||||
})
|
||||
}
|
||||
catch (err) {
|
||||
@@ -990,7 +1015,8 @@ export async function handleContinueBrainstorm(panel: KanbanWebviewPanel, issueN
|
||||
// 新建工单流程的 tab 叫 issue-new-{nonce}-规划,只能靠 newIssueTerminals 反查。
|
||||
const linked = panel.newIssueTerminals.get(issueNumber)
|
||||
const live = linked && linked.exitStatus === undefined ? linked : panel.findExistingTerminal(terminalName)
|
||||
const continuePath = pickBrainstormContinuePath({ liveTerminal: !!live, sessionId })
|
||||
const sessionFileExists = !!sessionId && await claudeSessionFileExists(sessionId)
|
||||
const continuePath = pickBrainstormContinuePath({ liveTerminal: !!live, sessionId, sessionFileExists })
|
||||
|
||||
if (continuePath === 'inject' && live) {
|
||||
panel.trackSessionTerminal(live, issueNumber, 'brainstorm')
|
||||
@@ -1390,6 +1416,10 @@ export async function handleResumeTestSession(panel: KanbanWebviewPanel, session
|
||||
existingByName.show(false)
|
||||
return
|
||||
}
|
||||
if (!(await claudeSessionFileExists(sessionId))) {
|
||||
rejectMissingSessionFile(panel, sessionId, issueNumber)
|
||||
return
|
||||
}
|
||||
|
||||
// 测试会话 resume 新建终端时也要触发 pre-create,与实施 resume / 首次
|
||||
// startTestSession 对称(开 pycharm 等);复用已有终端的 fast path 已在
|
||||
@@ -1668,6 +1698,10 @@ export async function handleResumePrototypeSession(panel: KanbanWebviewPanel, se
|
||||
existingByName.show(false)
|
||||
return
|
||||
}
|
||||
if (!(await claudeSessionFileExists(sessionId))) {
|
||||
rejectMissingSessionFile(panel, sessionId, issueNumber)
|
||||
return
|
||||
}
|
||||
|
||||
// 回退主 worktree 时不触发 pre-create——主 worktree 不该被 pycharm 指向,
|
||||
// 与实施 / 测试 resume 一致。
|
||||
|
||||
Reference in new issue
Block a user