refactor: rebuildRuntime 语义化——插件重载透明化,工作区切换全量切换

This commit is contained in:
Rick Ye
2026-09-10 12:03:16 +08:00
parent 4c5a379ca1
commit 8216d70fcf
3 changed files with 48 additions and 12 deletions
+3 -2
View File
@@ -82,8 +82,8 @@ async function createWindow(): Promise<void> {
})
// ready-to-show 在 Electron 中可能不止一次触发(实测:回合中途首次挂载受管浏览器
// WebContentsView 后会再次发出)。会话初始化只能做一次——二次触发会全量广播
// session_switched,把渲染层进行中的回合状态清掉(过程区误折叠/反复横跳的根因)。
// WebContentsView 后会再次发出)。会话初始化只做一次——但失败后放开门闩,
// 让后续的重复触发承担重试职责(该事件至少会再来一次)。
let sessionInitialized = false
win.on('ready-to-show', () => {
win.show()
@@ -91,6 +91,7 @@ async function createWindow(): Promise<void> {
sessionInitialized = true
// pi 会话初始化放在窗口就绪后,错误可以第一时间在 UI 上呈现
agentService.ensureReady().catch((err: unknown) => {
sessionInitialized = false
console.error('[main] pi session init failed:', err)
})
})
+1 -1
View File
@@ -241,8 +241,8 @@ export function registerIpc(): AgentService {
// ---- 工具审批 ----
ipcMain.handle('approval:resolve', (_event, id: unknown, approved: unknown) => {
if (typeof id !== 'string' || typeof approved !== 'boolean') throw new Error('invalid approval')
// settle 内部经 onSettle 订阅发出 approval_settled,此处无需重复广播
service.approvals.settle(id, approved)
service.broadcastEvent({ type: 'approval_settled', id, approved, timedOut: false })
return Promise.resolve()
})
+44 -9
View File
@@ -199,7 +199,8 @@ export class AgentService {
this.settings.workspace = resolved
this.saveSettings()
this.workspaceDir = resolved
await this.rebuildRuntime()
// 工作区切换:新 cwd 的最近会话才是目标(不做原会话恢复)
await this.rebuildRuntime({ restoreSession: false })
}
private saveSettings(): void {
@@ -215,11 +216,6 @@ export class AgentService {
return () => this.listeners.delete(fn)
}
/** 供 IPC 层主动广播事件(如审批结果同步到所有窗口) */
broadcastEvent(event: AgentUiEvent): void {
this.emit(event)
}
private emit(event: AgentUiEvent): void {
for (const fn of this.listeners) {
try {
@@ -1130,11 +1126,26 @@ export class AgentService {
}
}
/** 重建运行时(插件重载/启停后由 PluginHost 调用,extensionFactories 重跑) */
async rebuildRuntime(): Promise<void> {
/** 重建运行时(插件重载/启停、工作区切换时调用,extensionFactories 重跑)。
* restoreSession=true(插件重载):重建对渲染层尽量透明——
* - 进行中的回合先经 abort 正常结算(事件照常送达,渲染层自行收口),
* 再退订/销毁旧运行时——dispose 不含 abort,直接销毁会留下悬空请求;
* - 重建走 continueRecent(最近会话):用户开着的不是它时切回原会话;
* 原会话是未落盘的空新会话时退回空会话,避免静默接管别的会话;
* - 会话未变化时只推轻量 state,不广播 session_switched(该事件语义
* 保留给真正的会话变更,全量换血会重挂载渲染层全部消息)。
* restoreSession=false(工作区切换):新 cwd 的最近会话即目标状态,
* 直接全量广播切换。 */
async rebuildRuntime(options?: { restoreSession?: boolean }): Promise<void> {
const restoreSession = options?.restoreSession !== false
const old = this.runtime
this.runtime = null
this.initPromise = null
if (old?.session.isStreaming) {
await old.session.abort().catch(() => undefined)
}
// abort 结算会落盘当前回合,未持久会话的文件路径此时才物化——在其后读取
const previousPath = old?.session.sessionFile ?? null
this.unsubscribeAttached?.()
this.unsubscribeAttached = null
this.attached = null
@@ -1143,7 +1154,31 @@ export class AgentService {
console.error('[agent-service] runtime dispose error:', err)
})
}
await this.ensureReady()
let runtime: AgentSessionRuntime
try {
runtime = await this.getRuntime()
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
this.emit({ type: 'session_error', message })
throw err
}
if (!restoreSession) {
this.emitSwitched(runtime.session)
return
}
const restored = runtime.session.sessionFile ?? ''
if (previousPath) {
if (!samePath(restored, previousPath) && existsSync(previousPath)) {
// 被拽到了最近会话:切回用户原本打开的会话(rebind 发 session_switched,真实变更)
await runtime.switchSession(previousPath)
return
}
} else if (runtime.session.messages.length > 0) {
// 原会话是未落盘的空新会话:不接管最近会话,回到空会话
await runtime.newSession()
return
}
this.emit({ type: 'state', state: this.buildState(runtime.session) })
}
}