🐛 fix(vscode): 恢复实施会话改按显式角色命名,worktree 清理后不再误标为规划

Claude-Session: https://claude.ai/code/session_012YFEAU7RsuCdinUNMS9tzv
This commit is contained in:
cruldra committed 2026-09-09 10:41:04 +08:00
1 parent 1fee0f2630
commit e8963ac2a2
11 files changed
+116 -61

No files matched your search

+28 -50
View File
@@ -1,5 +1,6 @@
import type { Terminal } from 'vscode'
import type { KanbanWebviewPanel } from '../KanbanPanel'
import type { ResumeKind } from './resumeTarget'
import { execFile } from 'node:child_process'
import * as fs from 'node:fs'
import { promises as fsp } from 'node:fs'
@@ -25,13 +26,10 @@ import { decideAutoReviewPath } from '../autoReviewDecision'
import { pickBrainstormContinuePath } from '../brainstormContinue'
import { makeNonce } from '../KanbanPanel'
import { cleanupFeatureBranch } from './branchCleanup'
import { planResumeCwd, resumeTerminalName } from './resumeTarget'
import { submitToSessionTerminal } from './terminals'
export async function handleResumeSession(panel: KanbanWebviewPanel, sessionId: string, profilePath?: string, relCwd?: string, issueNumber?: number): Promise<void> {
// kind 跟着 sessionRole 提前判定(原来散落在方法中部,提到入口是为了构造
// resumeInFlight 锁 key)。relCwd 在则是实施 resume,否则视为规划/讨论。
const sessionKind: 'implement' | 'brainstorm' = relCwd ? 'implement' : 'brainstorm'
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 为空时
// (legacy session)跳过锁,直接走原路径。
@@ -91,45 +89,25 @@ export async function handleResumeSession(panel: KanbanWebviewPanel, sessionId:
return
}
const workspaceRoot = workspace.workspaceFolders?.[0]?.uri.fsPath
// Implementation sessions live in a worktree; `relCwd` may be absolute /
// `~/...` / workspace-relative — must go through resolveWorktreePath
// (path.join(workspaceRoot, abs) would mangle absolute paths).
// If the worktree has since been cleaned up (typical: drop card to
// "完成" → auto `git worktree remove`), fall back to the workspace
// root instead of pointing the terminal at a missing directory.
let effectiveCwd: string | undefined = workspaceRoot
let cwdFallback = false
if (relCwd && workspaceRoot) {
const resolved = resolveWorktreePath(relCwd, workspaceRoot)
if (fs.existsSync(resolved)) {
effectiveCwd = resolved
}
else {
cwdFallback = true
effectiveCwd = workspaceRoot
logger.add({
level: 'warn',
source: 'panel',
message: `worktree 不存在,退回工作区根目录 (#${issueNumber ?? '?'}): ${relCwd}`,
})
}
// 实施会话住在 worktree 里;relCwd 可能是绝对路径 / `~/...` / 工作区相对
// 路径,统一走 resolveWorktreePath。worktree 已被清理(典型:卡片拖到
// 「完成」→ 自动 `git worktree remove`,工单里 worktreePath 随之清空)
// 时退回工作区根目录;角色只看 sessionKind,不受 cwd 有无影响。
const worktreeAbs = relCwd && workspaceRoot ? resolveWorktreePath(relCwd, workspaceRoot) : undefined
const plan = workspaceRoot
? planResumeCwd({ kind: sessionKind, workspaceRoot, worktreeAbs, worktreeExists: !!worktreeAbs && fs.existsSync(worktreeAbs) })
: undefined
const effectiveCwd = plan?.cwd
const worktreeMissing = plan?.worktreeMissing ?? false
if (worktreeMissing) {
logger.add({
level: 'warn',
source: 'panel',
message: `worktree 不存在,退回工作区根目录 (#${issueNumber ?? '?'}): ${relCwd ?? '(工单未记录 worktreePath)'}`,
})
}
// Open the terminal as an editor tab beside the kanban (not in the
// bottom panel), so the user can see both side-by-side. `Beside` opens
// in a new editor group when needed.
// Name reflects which of the three session roles this is:
// relCwd present → 实施 session (resumed inside the worktree)
// relCwd absent → 规划 / discussion session (workspace root)
// Fall back to the legacy id-suffixed name if we somehow lack issueNumber.
const sessionRole = relCwd ? '实施' : '规划'
// 注:sessionKind 已在方法入口处判定(用于 resumeInFlight 锁 key)。
// 原来两条 trackSessionTerminal 路径都硬写 'brainstorm',导致实施 resume
// 后 terminalOrigin map 里 kind 错,onDidCloseTerminal 里的
// `kind === 'implement'` 判断永远不命中,impl-tab-post-close 钩子(关
// pycharm)不会执行。
const terminalName = issueNumber !== undefined
? `issue-${issueNumber}-${sessionRole}`
: `Claude · ${sessionId.slice(0, 8)}`
// 终端开成编辑区 tab(不在底部面板),和看板并排。
const terminalName = resumeTerminalName(sessionKind, sessionId, issueNumber)
// Scan live terminals before creating a new one — survives panel
// reload / webview rebuild where `this.terminals` got wiped but the
// tab is still open. Refresh the Map too so subsequent lookups (and
@@ -142,12 +120,12 @@ export async function handleResumeSession(panel: KanbanWebviewPanel, sessionId:
existingByName.show(false)
return
}
// 实施 resume 走新建 terminal 路径时(且 worktree 还在)需触发
// impl-tab-pre-create 钩子(启动 pycharm 等)。existingByName fast path
// 不调钩子——pycharm 应该已经在跑,重复启会冲突;cwdFallback=true 时
// worktree 已清理,pycharm 不该指向 workspace root;legacy session
// (issueNumber === undefined) 钩子签名要 issueNumber,也跳过。
if (sessionRole === '实施' && !cwdFallback && issueNumber !== undefined && workspaceRoot) {
// 实施 resume 走新建 terminal 路径且 worktree 还在时触发 impl-tab-pre-create
// 钩子(启动 pycharm 等)。existingByName fast path 不调——pycharm 应该已
// 在跑,重复启会冲突;worktree 没了也不调——钩子拿 WORKTREE_PATH 开
// pycharm,指向工作区根目录会把主仓库窗口拉起来;legacy session
// (issueNumber === undefined) 钩子签名要 issueNumber,同样跳过。
if (sessionKind === 'implement' && !worktreeMissing && issueNumber !== undefined && workspaceRoot) {
const settingsForHook = getSettings(panel.context)
// 从 state JSON 读 branch;任何一步读不到就用空字符串(脚本里
// BRANCH env 仍能传,只是为空),不阻塞 resume 主流程。
@@ -205,7 +183,7 @@ export async function handleResumeSession(panel: KanbanWebviewPanel, sessionId:
})
// Worktree 已清理时给用户一个明确提示,避免他们误以为 cc 在原
// worktree 路径里跑。toast 上 dismissOnTimer 5s 自动消失。
if (cwdFallback) {
if (worktreeMissing) {
panel.postMessage({
type: 'toast/show',
id: makeNonce(),