✨ feat(vscode): 历史会话可换 profile 恢复
This commit is contained in:
@@ -556,7 +556,7 @@ export class KanbanWebviewPanel {
|
||||
return
|
||||
}
|
||||
if (msg.type === 'managed-sessions/resume') {
|
||||
void managedSessions.handleManagedSessionsResume(this, msg.sessionId)
|
||||
void managedSessions.handleManagedSessionsResume(this, msg.sessionId, msg.profilePath)
|
||||
return
|
||||
}
|
||||
if (msg.type === 'managed-sessions/delete') {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { buildCcCommand } from '../../cc/ccCommand'
|
||||
import { pollForNewSession, projectsDirFor } from '../../cc/sessionWatcher'
|
||||
import { logger } from '../../logging/logger'
|
||||
import { listImportableNamedSessions } from '../../sessions/importableNamedSessions'
|
||||
import { decideManagedResumeProfile } from '../../sessions/managedResumeProfile'
|
||||
import { readManagedSessions, writeManagedSessions } from '../../sessions/managedStore'
|
||||
import { inferProfilePathForSession } from '../../sessions/sessionProfileInference'
|
||||
import { resolveProfilePath } from '../../cc/profiles'
|
||||
@@ -225,7 +226,7 @@ export async function handleManagedSessionsRename(panel: KanbanWebviewPanel, ses
|
||||
* 恢复一个受管理会话:在新终端 tab 里跑 `claude ... --resume <id>`,
|
||||
* cwd = workspaceRoot。in-flight 锁防止重复点击重复 createTerminal。
|
||||
*/
|
||||
export async function handleManagedSessionsResume(panel: KanbanWebviewPanel, sessionId: string): Promise<void> {
|
||||
export async function handleManagedSessionsResume(panel: KanbanWebviewPanel, sessionId: string, profilePath?: string): Promise<void> {
|
||||
const lockKey = `managed:${sessionId}`
|
||||
if (panel.resumeInFlight.has(lockKey)) {
|
||||
logger.add({
|
||||
@@ -244,29 +245,35 @@ export async function handleManagedSessionsResume(panel: KanbanWebviewPanel, ses
|
||||
return
|
||||
}
|
||||
|
||||
const data = await readManagedSessions(workspaceRoot)
|
||||
const target = data.sessions.find(s => s.id === sessionId)
|
||||
|
||||
const override = profilePath?.trim() || undefined
|
||||
let inferred: string | null = null
|
||||
if (!override && (!target?.profilePath || target.profilePath.trim() === '') && target) {
|
||||
inferred = await inferProfilePathForSession(sessionId, projectsDirFor(workspaceRoot))
|
||||
}
|
||||
|
||||
const decided = decideManagedResumeProfile({
|
||||
storedProfile: target?.profilePath,
|
||||
overrideProfile: profilePath,
|
||||
inferredProfile: inferred,
|
||||
})
|
||||
if (decided.persist && target) {
|
||||
target.profilePath = decided.persist
|
||||
await writeManagedSessions(workspaceRoot, data)
|
||||
}
|
||||
|
||||
// 已有该会话的存活终端,直接聚焦、不再开新的(防重复)。
|
||||
const existing = panel.managedTerminals.get(sessionId)
|
||||
if (existing) {
|
||||
if (decided.persist)
|
||||
await pushManagedSessions(panel)
|
||||
existing.show(false)
|
||||
return
|
||||
}
|
||||
|
||||
const data = await readManagedSessions(workspaceRoot)
|
||||
const target = data.sessions.find(s => s.id === sessionId)
|
||||
|
||||
// store 缺 profilePath 时从 transcript 推断并回写,避免 resume 落到 default profile。
|
||||
let storedProfile = target?.profilePath
|
||||
if ((!storedProfile || storedProfile.trim() === '') && target) {
|
||||
const inferred = await inferProfilePathForSession(sessionId, projectsDirFor(workspaceRoot))
|
||||
if (inferred) {
|
||||
storedProfile = inferred
|
||||
target.profilePath = inferred
|
||||
await writeManagedSessions(workspaceRoot, data)
|
||||
}
|
||||
}
|
||||
const effectiveProfilePath = resolveProfilePath(
|
||||
storedProfile && storedProfile.trim() !== '' ? storedProfile : undefined,
|
||||
)
|
||||
const effectiveProfilePath = resolveProfilePath(decided.effective)
|
||||
if (effectiveProfilePath.includes('\'')) {
|
||||
void window.showErrorMessage(
|
||||
`resume 失败:profilePath 含单引号,拒绝执行 (${effectiveProfilePath})`,
|
||||
|
||||
@@ -191,7 +191,7 @@ export type WebviewToExtension
|
||||
| { type: 'managed-sessions/get' }
|
||||
| { type: 'managed-sessions/create', profilePath: string, name?: string, prompt?: string }
|
||||
| { type: 'managed-sessions/rename', sessionId: string, name: string }
|
||||
| { type: 'managed-sessions/resume', sessionId: string }
|
||||
| { type: 'managed-sessions/resume', sessionId: string, profilePath?: string }
|
||||
| { type: 'managed-sessions/delete', sessionId: string }
|
||||
| { type: 'managed-sessions/close-tab', sessionId: string }
|
||||
| { type: 'managed-sessions/list-importable' }
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { decideManagedResumeProfile } from './managedResumeProfile'
|
||||
|
||||
describe('decideManagedResumeProfile', () => {
|
||||
it('override 非空时用 override,与 stored 不同则写回', () => {
|
||||
expect(decideManagedResumeProfile({
|
||||
storedProfile: '/profiles/grok-4.5.json',
|
||||
overrideProfile: '/profiles/opus.json',
|
||||
inferredProfile: '/profiles/from-jsonl.json',
|
||||
})).toEqual({
|
||||
effective: '/profiles/opus.json',
|
||||
persist: '/profiles/opus.json',
|
||||
})
|
||||
})
|
||||
|
||||
it('override 与 stored trim 后相同则不写回,且忽略 inferred', () => {
|
||||
expect(decideManagedResumeProfile({
|
||||
storedProfile: ' /profiles/grok-4.5.json ',
|
||||
overrideProfile: '/profiles/grok-4.5.json',
|
||||
inferredProfile: '/profiles/from-jsonl.json',
|
||||
})).toEqual({
|
||||
effective: '/profiles/grok-4.5.json',
|
||||
persist: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
it('空白 override 视为未提供,回落到 stored', () => {
|
||||
expect(decideManagedResumeProfile({
|
||||
storedProfile: '/profiles/grok-4.5.json',
|
||||
overrideProfile: ' ',
|
||||
inferredProfile: '/profiles/from-jsonl.json',
|
||||
})).toEqual({
|
||||
effective: '/profiles/grok-4.5.json',
|
||||
persist: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
it('无 override 时用 stored,不写回也不用 inferred', () => {
|
||||
expect(decideManagedResumeProfile({
|
||||
storedProfile: '/profiles/grok-4.5.json',
|
||||
overrideProfile: undefined,
|
||||
inferredProfile: '/profiles/from-jsonl.json',
|
||||
})).toEqual({
|
||||
effective: '/profiles/grok-4.5.json',
|
||||
persist: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
it('override 与 stored 都空时用 inferred 并写回', () => {
|
||||
expect(decideManagedResumeProfile({
|
||||
storedProfile: ' ',
|
||||
overrideProfile: undefined,
|
||||
inferredProfile: '/profiles/from-jsonl.json',
|
||||
})).toEqual({
|
||||
effective: '/profiles/from-jsonl.json',
|
||||
persist: '/profiles/from-jsonl.json',
|
||||
})
|
||||
})
|
||||
|
||||
it('三者都空则 effective 与 persist 都为 undefined', () => {
|
||||
expect(decideManagedResumeProfile({
|
||||
storedProfile: undefined,
|
||||
overrideProfile: '',
|
||||
inferredProfile: null,
|
||||
})).toEqual({
|
||||
effective: undefined,
|
||||
persist: undefined,
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,20 @@
|
||||
export function decideManagedResumeProfile(input: {
|
||||
storedProfile: string | undefined
|
||||
overrideProfile: string | undefined
|
||||
inferredProfile: string | null
|
||||
}): { effective: string | undefined, persist: string | undefined } {
|
||||
const override = input.overrideProfile?.trim() || undefined
|
||||
const stored = input.storedProfile?.trim() || undefined
|
||||
|
||||
if (override) { // ① 行内覆盖:与 store 不同才写回;忽略 jsonl 推断
|
||||
return {
|
||||
effective: override,
|
||||
persist: override !== stored ? override : undefined,
|
||||
}
|
||||
}
|
||||
if (stored) // ② store 已有 → 沿用,不写回
|
||||
return { effective: stored, persist: undefined }
|
||||
if (input.inferredProfile) // ③ 缺 profile 才用 jsonl 回填并写回
|
||||
return { effective: input.inferredProfile, persist: input.inferredProfile }
|
||||
return { effective: undefined, persist: undefined }
|
||||
}
|
||||
Reference in New Issue
Block a user