From 8216d70fcf1d265f55953b260e4db0ba125874a4 Mon Sep 17 00:00:00 2001 From: Rick Ye Date: Thu, 10 Sep 2026 12:03:16 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20rebuildRuntime=20=E8=AF=AD=E4=B9=89?= =?UTF-8?q?=E5=8C=96=E2=80=94=E2=80=94=E6=8F=92=E4=BB=B6=E9=87=8D=E8=BD=BD?= =?UTF-8?q?=E9=80=8F=E6=98=8E=E5=8C=96=EF=BC=8C=E5=B7=A5=E4=BD=9C=E5=8C=BA?= =?UTF-8?q?=E5=88=87=E6=8D=A2=E5=85=A8=E9=87=8F=E5=88=87=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/desktop/src/main/index.ts | 5 ++- apps/desktop/src/main/ipc.ts | 2 +- apps/desktop/src/main/pi/agent-service.ts | 53 +++++++++++++++++++---- 3 files changed, 48 insertions(+), 12 deletions(-) diff --git a/apps/desktop/src/main/index.ts b/apps/desktop/src/main/index.ts index f249c22..1eef9fe 100644 --- a/apps/desktop/src/main/index.ts +++ b/apps/desktop/src/main/index.ts @@ -82,8 +82,8 @@ async function createWindow(): Promise { }) // 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 { sessionInitialized = true // pi 会话初始化放在窗口就绪后,错误可以第一时间在 UI 上呈现 agentService.ensureReady().catch((err: unknown) => { + sessionInitialized = false console.error('[main] pi session init failed:', err) }) }) diff --git a/apps/desktop/src/main/ipc.ts b/apps/desktop/src/main/ipc.ts index 9f7c7a1..443ffa2 100644 --- a/apps/desktop/src/main/ipc.ts +++ b/apps/desktop/src/main/ipc.ts @@ -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() }) diff --git a/apps/desktop/src/main/pi/agent-service.ts b/apps/desktop/src/main/pi/agent-service.ts index eef4808..e8133e4 100644 --- a/apps/desktop/src/main/pi/agent-service.ts +++ b/apps/desktop/src/main/pi/agent-service.ts @@ -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 { + /** 重建运行时(插件重载/启停、工作区切换时调用,extensionFactories 重跑)。 + * restoreSession=true(插件重载):重建对渲染层尽量透明—— + * - 进行中的回合先经 abort 正常结算(事件照常送达,渲染层自行收口), + * 再退订/销毁旧运行时——dispose 不含 abort,直接销毁会留下悬空请求; + * - 重建走 continueRecent(最近会话):用户开着的不是它时切回原会话; + * 原会话是未落盘的空新会话时退回空会话,避免静默接管别的会话; + * - 会话未变化时只推轻量 state,不广播 session_switched(该事件语义 + * 保留给真正的会话变更,全量换血会重挂载渲染层全部消息)。 + * restoreSession=false(工作区切换):新 cwd 的最近会话即目标状态, + * 直接全量广播切换。 */ + async rebuildRuntime(options?: { restoreSession?: boolean }): Promise { + 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) }) } }