⚡ perf(vscode): 开会话去掉多余远程往返,spinner 跟随真实进度

开会话原先要先全量拉工单(含每张工单的评论、依赖、PR)做前置锁检查,
再各读一次 state JSON 给 pre-create 钩子和终端图标,几秒后才 createTerminal。

- 锁检查改用主进程已算好的工单快照,快照里没有该工单才回落全量拉取
- state JSON 一次读两处用,且提前发起与本地准备重叠
- 会话行 spinner 由扩展的 session/tab-settled 消息驱动,不再是固定 1.5s 假动画,
  消息丢失时 60s 超时兜底

Claude-Session: https://claude.ai/code/session_01Kgmb1LvrPmFX3NoUDB4zZC
This commit is contained in:
cruldra committed 2026-09-13 10:14:48 +08:00
1 parent b182e0ac5b
commit 70b826123c
12 files changed
+218 -71

No files matched your search

+12 -22
View File
@@ -1,4 +1,5 @@
import type { IssueColumn } from '../../gitea/types'
import type { LockedReason } from '../issueLock'
import type { KanbanWebviewPanel } from '../KanbanPanel'
import { Buffer } from 'node:buffer'
import { randomUUID } from 'node:crypto'
@@ -35,6 +36,7 @@ import { logger } from '../../logging/logger'
import { getEffectivePrDiffSummaryProfilePath, getSettings } from '../../settings/store'
import { webhookCoordinator } from '../../webhook/coordinator'
import { pickRandomIssueColor, themeColorIdToIconUri } from '../issueColor'
import { evaluateLockedReason } from '../issueLock'
import { makeNonce } from '../KanbanPanel'
import { cleanupFeatureBranch } from './branchCleanup'
import * as sessions from './sessions'
@@ -1572,12 +1574,10 @@ export async function syncCloseGiteaIssue(panel: KanbanWebviewPanel, opts: {
}
/**
* Server-side lock check for prerequisite gating. Fetches a fresh issues
* snapshot (we don't trust webview state) and reports whether
* `issueNumber` is blocked by an unfinished prerequisite. Mirrors the
* webview-side `isIssueLocked` predicate: locked iff the issue has a
* prerequisite that exists in the current snapshot and is not yet in the
* `done` column.
* Server-side lock check for prerequisite gating(不信 webview 传来的状态)。
* 优先用主进程自己算好的工单快照——开会话是热路径,全量 loadIssues 要拉工单
* 列表、每张工单的评论、依赖和 PR,几秒起步;快照没有该工单(面板还没加载
* 完)时才退回一次全量拉取。
*
* Fail-open: any error fetching issues is logged and we return
* `{ locked: false }` so a transient Gitea hiccup can't lock the user out
@@ -1586,8 +1586,12 @@ export async function syncCloseGiteaIssue(panel: KanbanWebviewPanel, opts: {
export async function resolveLockedReason(
panel: KanbanWebviewPanel,
issueNumber: number,
): Promise<{ locked: boolean, prerequisiteNumber?: number, prerequisiteColumn?: string }> {
): Promise<LockedReason> {
try {
const snapshot = panel.snapshotIssues()
if (snapshot?.some(i => i.number === issueNumber))
return evaluateLockedReason(snapshot, issueNumber)
const workspaceRoot = workspace.workspaceFolders?.[0]?.uri.fsPath
if (!workspaceRoot)
return { locked: false }
@@ -1604,21 +1608,7 @@ export async function resolveLockedReason(
repo: remote.repo,
workspaceRoot,
})
const issue = issues.find(i => i.number === issueNumber)
if (!issue)
return { locked: false }
if (issue.prerequisite === undefined)
return { locked: false }
const prereq = issues.find(i => i.number === issue.prerequisite)
if (!prereq)
return { locked: false }
if (prereq.column === 'done')
return { locked: false }
return {
locked: true,
prerequisiteNumber: prereq.number,
prerequisiteColumn: prereq.column,
}
return evaluateLockedReason(issues, issueNumber)
}
catch (err) {
const message = err instanceof Error ? err.message : String(err)
+23 -23
View File
@@ -73,6 +73,20 @@ export async function handleResumeSession(panel: KanbanWebviewPanel, sessionId:
existing.show(false)
return
}
// state JSON 一次读、两处用(钩子的 branch + 终端图标颜色)。提前发起,
// 让这趟远程往返和后面的本地准备工作重叠;读失败为 null,两处各自退化。
const statePromise: Promise<Record<string, unknown> | null> | undefined
= issueNumber !== undefined
? panel.readIssueState(issueNumber).catch((err) => {
logger.add({
level: 'warn',
source: 'panel',
message: `resume #${issueNumber} 读 state JSON 失败`,
details: err instanceof Error ? err.message : String(err),
})
return null
})
: undefined
// 实施 / 头脑风暴各自独立:调用方传入对应字段,空 = DEFAULT。
const effectiveProfilePath
= sessionKind === 'implement'
@@ -127,28 +141,10 @@ export async function handleResumeSession(panel: KanbanWebviewPanel, sessionId:
// (issueNumber === undefined) 钩子签名要 issueNumber,同样跳过。
if (sessionKind === 'implement' && !worktreeMissing && issueNumber !== undefined && workspaceRoot) {
const settingsForHook = getSettings(panel.context)
// 从 state JSON 读 branch;任何一步读不到就用空字符串(脚本里
// BRANCH env 仍能传,只是为空),不阻塞 resume 主流程。
let branchForHook = ''
try {
const remote = await detectRepo(workspaceRoot)
if (remote) {
const token = await getToken(panel.context, remote.host)
if (token) {
const stateObj = await panel.readIssueState(issueNumber)
if (stateObj && typeof stateObj.branch === 'string')
branchForHook = stateObj.branch
}
}
}
catch (err) {
logger.add({
level: 'warn',
source: 'panel',
message: `resume #${issueNumber} 读 state JSON 失败,钩子 branch 用空字符串`,
details: err instanceof Error ? err.message : String(err),
})
}
// 从 state JSON 读 branch;读不到就用空字符串(脚本里 BRANCH env 仍能
// 传,只是为空),不阻塞 resume 主流程。
const stateObj = await statePromise
const branchForHook = typeof stateObj?.branch === 'string' ? stateObj.branch : ''
await panel.dispatchWorktreeHook('impl-tab-pre-create', {
workspaceRoot,
worktreePath: effectiveCwd!,
@@ -164,7 +160,7 @@ export async function handleResumeSession(panel: KanbanWebviewPanel, sessionId:
// harmless in the editor tab, still useful if the same terminal ever
// gets rendered in panel-view mode.
const issueIcon = issueNumber !== undefined
? await panel.resolveIssueIcon(issueNumber)
? await panel.resolveIssueIcon(issueNumber, await statePromise)
: undefined
const terminal = window.createTerminal({
name: terminalName,
@@ -198,6 +194,8 @@ export async function handleResumeSession(panel: KanbanWebviewPanel, sessionId:
finally {
if (lockKey !== undefined)
panel.resumeInFlight.delete(lockKey)
// 无论开成、复用已有 tab 还是被前置锁拒绝,都要告诉 webview 收 spinner。
panel.postMessage({ type: 'session/tab-settled', sessionId })
}
}
@@ -303,6 +301,7 @@ export async function handleResumeReviewSession(panel: KanbanWebviewPanel, sessi
}
finally {
panel.resumeInFlight.delete(lockKey)
panel.postMessage({ type: 'session/tab-settled', sessionId })
}
}
@@ -1379,6 +1378,7 @@ export async function handleResumeTestSession(panel: KanbanWebviewPanel, session
}
finally {
panel.resumeInFlight.delete(lockKey)
panel.postMessage({ type: 'session/tab-settled', sessionId })
}
}