🐛 fix(vscode): 别的机器上开的会话在本机没有会话文件时,会话 id 行置灰不可双击,不再 resume 报错

This commit is contained in:
cruldra committed 2026-09-24 19:22:29 +08:00
1 parent 2b1e660cc8
commit bf4661e364
20 files changed
+508 -234

No files matched your search

@@ -5,11 +5,15 @@ import { ArrowRightLeft, CircleSlash, Download, Eraser, ExternalLink, GitMerge,
import { useMemo, useRef } from 'react'
import { isIssueLocked } from '../lib/dependencies'
import { isIssueMine, issueOwnerLogin } from '../lib/issueOwnership'
import { isSessionMissing } from '../lib/missingSession'
import { toReadOnlySchema } from '../lib/readOnlySchema'
import { COLUMN_LABELS, COLUMN_ORDER } from '../types'
import { PropertyGrid } from './property-grid'
import { Tooltip } from './ui/tooltip'
/** 会话 id 在共享状态里、本机却没有会话文件时,各会话行的统一说明。 */
const MISSING_SESSION_DESC = '本机没有该会话文件(会话在别的机器上创建),无法 resume'
/** 跨机绝对路径只比 basename(同名 .json)映射到本机 profiles 列表。 */
function matchProfileOption(
current: string | undefined,
@@ -194,10 +198,10 @@ export function IssueDetailPanel({
: undefined
const prDiffSummaryRunning = issue ? isPrDiffSummaryRunning(issue.number) : false
// 三个会话 id 行(头脑风暴/实施/审查)共用一个"上次触发时间戳"表,
// 各会话 id 行(头脑风暴/实施/审查/测试/原型)共用一个"上次触发时间戳"表,
// 防止用户连续快速双击同一行时触发多次 resume —— extension 端 findExistingTerminal
// 在前一个 createTerminal 尚未完成时查不到已存在的终端,会重复开 tab。
// key 用 sessionId 字符串本身(足够区分三种行 + 不同工单)。
// key 用 sessionId 字符串本身(足够区分不同行 + 不同工单)。
const lastFiredAtRef = useRef<Map<string, number>>(new Map())
const RESUME_THROTTLE_MS = 800
const throttleBySessionId = (sid: string): boolean => {
@@ -242,9 +246,12 @@ export function IssueDetailPanel({
// - 工单未完成 → 一直有 ▶ 续聊。插件把续聊提示词送进对应会话:
// 规划终端开着就直接打进去,关了就 --resume 带提示词,没会话就新开。
// - brainstormTabOpen → 再加一个 X 关闭 tab
// - 会话 id 本机没有会话文件 → 只读不可双击,续聊会新开会话
const hasSession = !!(issue?.sessionId && issue.sessionId.length > 0)
const missing = isSessionMissing(issue, issue?.sessionId)
const canContinue = issue?.column !== 'done'
const secondaryActions: RowAction[] = []
if (issue?.column !== 'done') {
if (canContinue) {
secondaryActions.push({
icon: <Play className="size-3.5" />,
title: '续聊:把续聊提示词发给头脑风暴会话',
@@ -274,6 +281,17 @@ export function IssueDetailPanel({
},
})
}
if (missing) {
return {
key: 'sessionId',
label: '头脑风暴会话id',
type: 'action' as const,
readOnly: true,
description: canContinue ? `${MISSING_SESSION_DESC};点 ▶ 续聊会新开一个规划会话` : MISSING_SESSION_DESC,
actionIcon: <Terminal className="size-3.5" />,
secondaryActions,
}
}
return {
key: 'sessionId',
label: '头脑风暴会话id',
@@ -293,27 +311,14 @@ export function IssueDetailPanel({
secondaryActions,
}
})(),
// 实施会话即使 worktree 已被清掉也保留可双击的 resume 入口:
// extension 端 handleResumeSession 会在 worktree 路径不存在时退回
// 工作区根目录,并通过 toast 告知用户;description 在两种状态下
// 给出不同的提示文案。
{
key: 'implementSessionId',
label: '实施会话id',
type: 'action',
pending: issue?.implementSessionId ? isSessionTabPending(issue.implementSessionId) : false,
description: issue?.worktreeExists
? '双击在新终端运行 claude --resume <id> 恢复实施对话(cwd 为 worktree)'
: 'worktree 已清理,将在工作区根目录恢复(cc 可能提示原 cwd 不存在)',
actionIcon: <Terminal className="size-3.5" />,
onAction: (v) => {
if (typeof v !== 'string' || v.length === 0)
return
if (!throttleBySessionId(v))
return
onResumeSession(v, 'implement', issue?.profilePath, issue?.worktreePath, issue?.number)
},
secondaryActions: (() => {
(() => {
// 实施会话即使 worktree 已被清掉也保留可双击的 resume 入口:
// extension 端 handleResumeSession 会在 worktree 路径不存在时退回
// 工作区根目录,并通过 toast 告知用户;description 在两种状态下
// 给出不同的提示文案。
// 本机没有会话文件时只读且不给 ▶:重新实施会先删远程 feature 分支,
// 会把别的机器上的实施进度一并抹掉。
const secondaryActions: RowAction[] = (() => {
if (issue?.implementTabOpen) {
return [{
icon: <X className="size-3.5" />,
@@ -333,9 +338,38 @@ export function IssueDetailPanel({
|| locked,
onClick: () => onImplement(issue.number, issue.planFile ?? '', issue.profilePath, issue.sessionId),
}]
})(),
},
issue?.reviewSessionFileExists
})()
if (isSessionMissing(issue, issue?.implementSessionId)) {
return {
key: 'implementSessionId',
label: '实施会话id',
type: 'action' as const,
readOnly: true,
description: `${MISSING_SESSION_DESC};要在本机接着实施,请让对方把工单移交过来`,
actionIcon: <Terminal className="size-3.5" />,
secondaryActions,
}
}
return {
key: 'implementSessionId',
label: '实施会话id',
type: 'action' as const,
pending: issue?.implementSessionId ? isSessionTabPending(issue.implementSessionId) : false,
description: issue?.worktreeExists
? '双击在新终端运行 claude --resume <id> 恢复实施对话(cwd 为 worktree)'
: 'worktree 已清理,将在工作区根目录恢复(cc 可能提示原 cwd 不存在)',
actionIcon: <Terminal className="size-3.5" />,
onAction: (v: unknown) => {
if (typeof v !== 'string' || v.length === 0)
return
if (!throttleBySessionId(v))
return
onResumeSession(v, 'implement', issue?.profilePath, issue?.worktreePath, issue?.number)
},
secondaryActions,
}
})(),
issue?.reviewSessionId && !isSessionMissing(issue, issue.reviewSessionId)
? {
key: 'reviewSessionId',
label: '审查会话id',
@@ -364,7 +398,7 @@ export function IssueDetailPanel({
type: 'action',
readOnly: true,
description: issue?.reviewSessionId
? '本机没有该 codex 会话文件,无法 resume;点 ▶ 对当前 PR 重新起一个审查会话'
? `${MISSING_SESSION_DESC};点 ▶ 对当前 PR 重新起一个审查会话`
: '还没有审查会话;点 ▶ 对当前 PR 启动 codex 审查(等同 PR 事件自动触发的那次)',
secondaryActions: issue?.pr
? [{
@@ -383,8 +417,11 @@ export function IssueDetailPanel({
// 条件与 handleStartTestSession 一致:worktree 还在就进 worktree 测,
// 代码本就在工作目录,不需要 PR;worktree 已清理才回退主工作区,
// 那条路径要靠 PR 让 cc 了解合并进来的改动。
// 会话 id 本机没有会话文件 → 只读不可双击,按状态 3 的条件给 ▶ 重新起一个
// (启动会覆盖旧 testSessionId)。
const hasSession = !!(issue?.testSessionId && issue.testSessionId.length > 0)
const canStart = !hasSession && (!!issue?.worktreeExists || !!issue?.pr)
const missing = isSessionMissing(issue, issue?.testSessionId)
const canStart = (!hasSession || missing) && (!!issue?.worktreeExists || !!issue?.pr)
const secondaryActions: RowAction[] = []
if (hasSession && issue?.testTabOpen) {
secondaryActions.push({
@@ -406,6 +443,17 @@ export function IssueDetailPanel({
},
})
}
if (missing) {
return {
key: 'testSessionId',
label: '测试会话id',
type: 'action' as const,
readOnly: true,
description: `${MISSING_SESSION_DESC};点 ▶ 新起一个测试会话(worktree 还在就在 worktree 里测,已清理则需要工单有已合并的 PR)`,
actionIcon: <Terminal className="size-3.5" />,
secondaryActions,
}
}
return {
key: 'testSessionId',
label: '测试会话id',
@@ -428,7 +476,10 @@ export function IssueDetailPanel({
(() => {
// 原型会话 id 行:与测试行同构的三态,差别是启动不看 PR——原型只照
// 工单正文画界面,任何阶段都能起;前置工单没做完时按钮禁用。
// 会话 id 本机没有会话文件 → 只读不可双击,同样给 ▶ 重新起一个
// (启动会覆盖旧 prototypeSessionId)。
const hasSession = !!(issue?.prototypeSessionId && issue.prototypeSessionId.length > 0)
const missing = isSessionMissing(issue, issue?.prototypeSessionId)
const secondaryActions: RowAction[] = []
if (hasSession && issue?.prototypeTabOpen) {
secondaryActions.push({
@@ -440,7 +491,7 @@ export function IssueDetailPanel({
},
})
}
else if (!hasSession) {
else if (!hasSession || missing) {
secondaryActions.push({
icon: <Play className="size-3.5" />,
title: locked ? lockTitle : '启动原型会话',
@@ -451,6 +502,17 @@ export function IssueDetailPanel({
},
})
}
if (missing) {
return {
key: 'prototypeSessionId',
label: '原型会话id',
type: 'action' as const,
readOnly: true,
description: `${MISSING_SESSION_DESC};点 ▶ 新起一个原型会话`,
actionIcon: <Terminal className="size-3.5" />,
secondaryActions,
}
}
return {
key: 'prototypeSessionId',
label: '原型会话id',