From 58b9e477bc83e42b7478dd8bdfeacae2b346b011 Mon Sep 17 00:00:00 2001 From: yiliang114 <1204183885@qq.com> Date: Tue, 9 Dec 2025 00:14:35 +0800 Subject: [PATCH] feat(vscode-ide-companion): implement session rehydration for loading past conversations - Add rehydratingSessionId flag to track session loading state - Route message chunks as discrete messages during rehydration instead of streaming - Update session handlers to properly manage conversation switching - Improve session manager documentation --- .../src/services/qwenAgentManager.ts | 63 +++++++++++++++++++ .../src/services/qwenSessionManager.ts | 4 ++ .../webview/handlers/SessionMessageHandler.ts | 28 ++++----- 3 files changed, 79 insertions(+), 16 deletions(-) diff --git a/packages/vscode-ide-companion/src/services/qwenAgentManager.ts b/packages/vscode-ide-companion/src/services/qwenAgentManager.ts index 1bc55f96..b4816d7f 100644 --- a/packages/vscode-ide-companion/src/services/qwenAgentManager.ts +++ b/packages/vscode-ide-companion/src/services/qwenAgentManager.ts @@ -37,6 +37,10 @@ export class QwenAgentManager { private connectionHandler: QwenConnectionHandler; private sessionUpdateHandler: QwenSessionUpdateHandler; private currentWorkingDir: string = process.cwd(); + // When loading a past session via ACP, the CLI replays history through + // session/update notifications. We set this flag to route message chunks + // (user/assistant) as discrete chat messages instead of live streaming. + private rehydratingSessionId: string | null = null; // Cache the last used AuthStateManager so internal calls (e.g. fallback paths) // can reuse it and avoid forcing a fresh authentication unnecessarily. private defaultAuthStateManager?: AuthStateManager; @@ -53,6 +57,55 @@ export class QwenAgentManager { // Set ACP connection callbacks this.connection.onSessionUpdate = (data: AcpSessionUpdate) => { + // If we are rehydrating a loaded session, map message chunks into + // full messages for the UI, instead of streaming behavior. + try { + const targetId = this.rehydratingSessionId; + if ( + targetId && + typeof data === 'object' && + data && + 'update' in data && + (data as { sessionId?: string }).sessionId === targetId + ) { + const update = ( + data as unknown as { + update: { sessionUpdate: string; content?: { text?: string } }; + } + ).update; + const text = update?.content?.text || ''; + if (update?.sessionUpdate === 'user_message_chunk' && text) { + console.log( + '[QwenAgentManager] Rehydration: routing user message chunk', + ); + this.callbacks.onMessage?.({ + role: 'user', + content: text, + timestamp: Date.now(), + }); + return; + } + if (update?.sessionUpdate === 'agent_message_chunk' && text) { + console.log( + '[QwenAgentManager] Rehydration: routing agent message chunk', + ); + this.callbacks.onMessage?.({ + role: 'assistant', + content: text, + timestamp: Date.now(), + }); + return; + } + // For other types during rehydration, fall through to normal handler + console.log( + '[QwenAgentManager] Rehydration: non-text update, forwarding to handler', + ); + } + } catch (err) { + console.warn('[QwenAgentManager] Rehydration routing failed:', err); + } + + // Default handling path this.sessionUpdateHandler.handleSessionUpdate(data); }; @@ -986,6 +1039,12 @@ export class QwenAgentManager { } try { + // Route upcoming session/update messages as discrete messages for replay + this.rehydratingSessionId = sessionId; + console.log( + '[QwenAgentManager] Rehydration start for session:', + sessionId, + ); console.log( '[QwenAgentManager] Attempting session/load via ACP for session:', sessionId, @@ -1032,6 +1091,10 @@ export class QwenAgentManager { } throw error; + } finally { + // End rehydration routing regardless of outcome + console.log('[QwenAgentManager] Rehydration end for session:', sessionId); + this.rehydratingSessionId = null; } } diff --git a/packages/vscode-ide-companion/src/services/qwenSessionManager.ts b/packages/vscode-ide-companion/src/services/qwenSessionManager.ts index c26cb26b..2bd609bb 100644 --- a/packages/vscode-ide-companion/src/services/qwenSessionManager.ts +++ b/packages/vscode-ide-companion/src/services/qwenSessionManager.ts @@ -15,6 +15,10 @@ import type { QwenSession, QwenMessage } from './qwenSessionReader.js'; * * This service provides direct filesystem access to save and load sessions * without relying on the CLI's ACP session/save method. + * + * Note: This is primarily used as a fallback mechanism when ACP methods are + * unavailable or fail. In normal operation, ACP session/list and session/load + * should be preferred for consistency with the CLI. */ export class QwenSessionManager { private qwenDir: string; diff --git a/packages/vscode-ide-companion/src/webview/handlers/SessionMessageHandler.ts b/packages/vscode-ide-companion/src/webview/handlers/SessionMessageHandler.ts index 65a40942..741d9684 100644 --- a/packages/vscode-ide-companion/src/webview/handlers/SessionMessageHandler.ts +++ b/packages/vscode-ide-companion/src/webview/handlers/SessionMessageHandler.ts @@ -598,24 +598,22 @@ export class SessionMessageHandler extends BaseMessageHandler { // Try to load session via ACP (now we should be connected) try { + // Set current id and clear UI first so replayed updates append afterwards + this.currentConversationId = sessionId; + this.sendToWebView({ + type: 'qwenSessionSwitched', + data: { sessionId, messages: [], session: sessionDetails }, + }); + const loadResponse = await this.agentManager.loadSessionViaAcp( sessionId, (sessionDetails?.cwd as string | undefined) || undefined, ); console.log( - '[SessionMessageHandler] session/load succeeded:', + '[SessionMessageHandler] session/load succeeded (per ACP spec result is null; actual history comes via session/update):', loadResponse, ); - this.currentConversationId = sessionId; - - const messages = await this.agentManager.getSessionMessages(sessionId); - - this.sendToWebView({ - type: 'qwenSessionSwitched', - data: { sessionId, messages, session: sessionDetails }, - }); - // Reset title flag when switching sessions this.isTitleSet = false; @@ -1029,17 +1027,15 @@ export class SessionMessageHandler extends BaseMessageHandler { // Try ACP load first try { - await this.agentManager.loadSessionViaAcp(sessionId); - + // Pre-clear UI so replayed updates append afterwards this.currentConversationId = sessionId; - - const messages = await this.agentManager.getSessionMessages(sessionId); - this.sendToWebView({ type: 'qwenSessionSwitched', - data: { sessionId, messages }, + data: { sessionId, messages: [] }, }); + await this.agentManager.loadSessionViaAcp(sessionId); + // Reset title flag when resuming sessions this.isTitleSet = false;