🐛 fix(vscode): 修复看板缓存并增加问答默认模型

This commit is contained in:
cruldra committed 2026-10-09 17:00:43 -04:00
1 parent c38f1dc468
commit 012daf95b5
25 files changed
+1753 -164

No files matched your search

@@ -1,5 +1,6 @@
import type { Terminal } from 'vscode'
import type { CodexSessionLaunch } from '../../cc/codexSessionWatcher'
import type { IssueRef } from '../../issues/stateRouter'
import type { KanbanWebviewPanel } from '../KanbanPanel'
import { execFile } from 'node:child_process'
import * as path from 'node:path'
@@ -11,6 +12,7 @@ import { logger } from '../../logging/logger'
import { defaultCodexSessionsDir } from '../../sessions/sessionFiles'
interface ReviewCapture {
source: NonNullable<IssueRef['source']>
abort: AbortController
launch: CodexSessionLaunch
terminal?: Terminal
@@ -18,7 +20,7 @@ interface ReviewCapture {
}
const captures = new WeakMap<KanbanWebviewPanel, Map<number, ReviewCapture>>()
const recoveries = new WeakMap<KanbanWebviewPanel, Map<number, { abort: AbortController, result: Promise<string | null> }>>()
const recoveries = new WeakMap<KanbanWebviewPanel, Map<number, { source: ReviewCapture['source'], abort: AbortController, result: Promise<string | null> }>>()
function captureMap(panel: KanbanWebviewPanel): Map<number, ReviewCapture> {
let map = captures.get(panel)
@@ -29,17 +31,20 @@ function captureMap(panel: KanbanWebviewPanel): Map<number, ReviewCapture> {
return map
}
function launchKey(panel: KanbanWebviewPanel, issueNumber: number): string {
return `superpowers.reviewSessionLaunch.${panel.refFor(issueNumber).source ?? 'gitea'}:${issueNumber}`
function launchKey(source: ReviewCapture['source'], issueNumber: number): string {
return `superpowers.reviewSessionLaunch.${source}:${issueNumber}`
}
/** 用户清除关联时先中止捕获,防止稍后落盘的文件把关联补回来。 */
export async function cancelReviewSessionCapture(panel: KanbanWebviewPanel, issueNumber: number): Promise<void> {
const map = captureMap(panel)
recoveries.get(panel)?.get(issueNumber)?.abort.abort()
map.get(issueNumber)?.abort.abort()
const capture = map.get(issueNumber)
const recovery = recoveries.get(panel)?.get(issueNumber)
const source = capture?.source ?? recovery?.source ?? panel.refFor(issueNumber).source ?? 'gitea'
recovery?.abort.abort()
capture?.abort.abort()
map.delete(issueNumber)
await panel.context.workspaceState.update(launchKey(panel, issueNumber), undefined)
await panel.context.workspaceState.update(launchKey(source, issueNumber), undefined)
}
export function stopReviewSessionCaptures(panel: KanbanWebviewPanel): void {
@@ -64,27 +69,32 @@ function reportCaptureFailure(issueNumber: number, err: unknown): void {
async function persistCapture(panel: KanbanWebviewPanel, issueNumber: number, capture: ReviewCapture, id: string): Promise<string | null> {
if (capture.abort.signal.aborted || captureMap(panel).get(issueNumber) !== capture || !capture.terminal)
return null
await mergeLocalIssueState(panel.context, panel.refFor(issueNumber).source, issueNumber, { reviewSessionId: id })
await mergeLocalIssueState(panel.context, capture.source, issueNumber, { reviewSessionId: id })
if (capture.abort.signal.aborted)
return null
panel.reviewTerminals.set(id, capture.terminal)
panel.trackSessionTerminal(capture.terminal, issueNumber, 'review')
panel.postMessage({ type: 'issue/patch', issueNumber, patch: { reviewSessionId: id } })
await panel.context.workspaceState.update(launchKey(panel, issueNumber), undefined)
await panel.context.workspaceState.update(launchKey(capture.source, issueNumber), undefined)
logger.add({ level: 'info', source: 'review', message: `已捕获审查会话 ${id} (#${issueNumber})` })
return id
}
/** 返回前完成目录确认和首次扫描,调用方随后才创建终端。 */
export async function prepareReviewSessionCapture(panel: KanbanWebviewPanel, issueNumber: number, launch: CodexSessionLaunch): Promise<{
export async function prepareReviewSessionCapture(
panel: KanbanWebviewPanel,
issueNumber: number,
launch: CodexSessionLaunch,
source: ReviewCapture['source'] = panel.refFor(issueNumber).source ?? 'gitea',
): Promise<{
attach: (terminal: Terminal) => Promise<string | null>
initialSessionId: string | null
}> {
captureMap(panel).get(issueNumber)?.abort.abort()
const capture: ReviewCapture = { abort: new AbortController(), launch }
const capture: ReviewCapture = { source, abort: new AbortController(), launch }
captureMap(panel).set(issueNumber, capture)
try {
await panel.context.workspaceState.update(launchKey(panel, issueNumber), launch)
await panel.context.workspaceState.update(launchKey(source, issueNumber), launch)
const watch = await watchForCodexSession({
...launch,
baseDir: defaultCodexSessionsDir(),
@@ -214,8 +224,8 @@ function terminalProcessId(terminal: Terminal, signal: AbortSignal): Promise<num
}
/** 已有文件立即补关联;尚未落盘则后台等待,刷新不会等完整审查运行。 */
async function recoverReviewSessionCaptureInner(panel: KanbanWebviewPanel, issueNumber: number, terminal: Terminal, signal: AbortSignal): Promise<string | null> {
const local = getLocalIssueState(panel.context, panel.refFor(issueNumber).source, issueNumber)
async function recoverReviewSessionCaptureInner(panel: KanbanWebviewPanel, issueNumber: number, terminal: Terminal, signal: AbortSignal, source: ReviewCapture['source']): Promise<string | null> {
const local = getLocalIssueState(panel.context, source, issueNumber)
if (local.reviewSessionId === '' || captureMap(panel).has(issueNumber))
return null
if (local.reviewSessionId)
@@ -230,11 +240,11 @@ async function recoverReviewSessionCaptureInner(panel: KanbanWebviewPanel, issue
const processStartedAt = await codexProcessStartedAt(pid, signal)
if (signal.aborted || terminal.exitStatus !== undefined)
return null
const stored = panel.context.workspaceState.get<CodexSessionLaunch>(launchKey(panel, issueNumber))
const stored = panel.context.workspaceState.get<CodexSessionLaunch>(launchKey(source, issueNumber))
const startedAt = stored?.prompt === launch.prompt && stored.cwd === launch.cwd && Number.isFinite(stored.startedAt)
? Math.max(stored.startedAt, processStartedAt)
: processStartedAt
const prepared = await prepareReviewSessionCapture(panel, issueNumber, { ...launch, startedAt })
const prepared = await prepareReviewSessionCapture(panel, issueNumber, { ...launch, startedAt }, source)
if (signal.aborted) {
await cancelReviewSessionCapture(panel, issueNumber)
return null
@@ -259,12 +269,13 @@ export function recoverReviewSessionCapture(panel: KanbanWebviewPanel, issueNumb
if (existing)
return existing.result
const abort = new AbortController()
const result = recoverReviewSessionCaptureInner(panel, issueNumber, terminal, abort.signal)
const source = panel.refFor(issueNumber).source ?? 'gitea'
const result = recoverReviewSessionCaptureInner(panel, issueNumber, terminal, abort.signal, source)
.finally(() => {
if (map.get(issueNumber)?.result === result)
map.delete(issueNumber)
})
map.set(issueNumber, { abort, result })
map.set(issueNumber, { source, abort, result })
return result
}
+5 -2
View File
@@ -45,6 +45,7 @@ export async function handleSettingsSave(panel: KanbanWebviewPanel, payload: {
defaultImplementProfilePath: string
defaultTestProfilePath: string
defaultPrototypeProfilePath: string
defaultAskProfilePath: string
conflictResolutionProfilePath: string
commitProfilePath: string
prDiffSummaryProfilePath: string
@@ -85,6 +86,7 @@ export async function handleSettingsSave(panel: KanbanWebviewPanel, payload: {
const trimmedDefaultImplementProfile = payload.defaultImplementProfilePath.trim()
const trimmedDefaultTestProfile = payload.defaultTestProfilePath.trim()
const trimmedDefaultPrototypeProfile = payload.defaultPrototypeProfilePath.trim()
const trimmedDefaultAskProfile = payload.defaultAskProfilePath.trim()
// conflict-resolution profile: '' is meaningful (= DEFAULT_PROFILE_PATH).
const trimmedConflictProfile = payload.conflictResolutionProfilePath.trim()
// 提交代码 profile:'' 有意义(= 未配置 → 提交严格失败),只 trim。
@@ -134,6 +136,7 @@ export async function handleSettingsSave(panel: KanbanWebviewPanel, payload: {
defaultImplementProfilePath: trimmedDefaultImplementProfile,
defaultTestProfilePath: trimmedDefaultTestProfile,
defaultPrototypeProfilePath: trimmedDefaultPrototypeProfile,
defaultAskProfilePath: trimmedDefaultAskProfile,
conflictResolutionProfilePath: trimmedConflictProfile,
commitProfilePath: trimmedCommitProfile,
prDiffSummaryProfilePath: trimmedPrDiffSummaryProfile,
@@ -168,6 +171,7 @@ export async function handleSettingsSave(panel: KanbanWebviewPanel, payload: {
defaultImplementProfilePath: trimmedDefaultImplementProfile,
defaultTestProfilePath: trimmedDefaultTestProfile,
defaultPrototypeProfilePath: trimmedDefaultPrototypeProfile,
defaultAskProfilePath: trimmedDefaultAskProfile,
conflictResolutionProfilePath: trimmedConflictProfile,
commitProfilePath: trimmedCommitProfile,
prDiffSummaryProfilePath: trimmedPrDiffSummaryProfile,
@@ -178,8 +182,6 @@ export async function handleSettingsSave(panel: KanbanWebviewPanel, payload: {
youtrackProjectShortName: trimmedYtProject,
youtrackCloseCommand: trimmedYtClose,
})
// 保存后立刻让 listClaudeProfiles / getDefaultProfilePath 吃到新目录。
setProfilesDirOverride(trimmedProfilesDirectory || undefined)
void handleProfilesList(panel)
if (!keepExisting) {
await setToken(panel.context, trimmedHost, trimmedToken)
@@ -279,6 +281,7 @@ export async function handleEditSettingsRequest(panel: KanbanWebviewPanel): Prom
defaultImplementProfilePath: s.defaultImplementProfilePath,
defaultTestProfilePath: s.defaultTestProfilePath,
defaultPrototypeProfilePath: s.defaultPrototypeProfilePath,
defaultAskProfilePath: s.defaultAskProfilePath,
conflictResolutionProfilePath: s.conflictResolutionProfilePath,
commitProfilePath,
prDiffSummaryProfilePath,