From 90fc4c33f076ed39300d0f774cb7496a26e346c3 Mon Sep 17 00:00:00 2001 From: yiliang114 <1204183885@qq.com> Date: Sat, 13 Dec 2025 20:06:02 +0800 Subject: [PATCH] fix(vscode-ide-companion/session): improve timeout configuration for different methods Extend timeout duration to 2 minutes for both session_prompt and initialize methods to prevent timeouts during longer operations. Default timeout remains at 60 seconds for other methods. This change improves reliability of session management by providing adequate time for initialization and prompt operations to complete. --- .../src/cli/cliVersionChecker.ts | 43 +-- .../src/services/qwenAgentManager.ts | 246 ++++++++++-------- .../src/utils/authNotificationHandler.ts | 10 - .../vscode-ide-companion/src/webview/App.tsx | 111 ++++---- .../src/webview/WebViewProvider.ts | 58 +---- .../webview/components/layout/Onboarding.tsx | 2 +- 6 files changed, 206 insertions(+), 264 deletions(-) diff --git a/packages/vscode-ide-companion/src/cli/cliVersionChecker.ts b/packages/vscode-ide-companion/src/cli/cliVersionChecker.ts index b5ffaaa6..6c93609d 100644 --- a/packages/vscode-ide-companion/src/cli/cliVersionChecker.ts +++ b/packages/vscode-ide-companion/src/cli/cliVersionChecker.ts @@ -6,7 +6,10 @@ import * as vscode from 'vscode'; import { CliDetector, type CliDetectionResult } from './cliDetector.js'; -import { CliVersionManager } from './cliVersionManager.js'; +import { + CliVersionManager, + MIN_CLI_VERSION_FOR_SESSION_METHODS, +} from './cliVersionManager.js'; import semver from 'semver'; /** @@ -78,15 +81,17 @@ export class CliVersionChecker { const isSupported = versionInfo.isSupported; // Check if update is needed (version is too old) - const minRequiredVersion = '0.5.0'; // This should match MIN_CLI_VERSION_FOR_SESSION_METHODS from CliVersionManager const needsUpdate = currentVersion - ? !semver.satisfies(currentVersion, `>=${minRequiredVersion}`) + ? !semver.satisfies( + currentVersion, + `>=${MIN_CLI_VERSION_FOR_SESSION_METHODS}`, + ) : false; // Show notification only if needed and within cooldown period if (showNotifications && !isSupported && this.canShowNotification()) { vscode.window.showWarningMessage( - `Qwen Code CLI version is outdated. Current: ${currentVersion || 'unknown'}, Minimum required: ${minRequiredVersion}. Please update using: npm install -g @qwen-code/qwen-code@latest`, + `Qwen Code CLI version ${currentVersion} is below the minimum required version. Some features may not work properly. Please upgrade to version ${MIN_CLI_VERSION_FOR_SESSION_METHODS} or later`, ); this.lastNotificationTime = Date.now(); } @@ -125,34 +130,4 @@ export class CliVersionChecker { CliVersionChecker.NOTIFICATION_COOLDOWN_MS ); } - - /** - * Clear notification cooldown (allows immediate next notification) - */ - clearCooldown(): void { - this.lastNotificationTime = 0; - } - - /** - * Get version status for display in status bar or other UI elements - */ - async getVersionStatus(): Promise { - try { - const versionManager = CliVersionManager.getInstance(); - const versionInfo = await versionManager.detectCliVersion(); - - if (!versionInfo.detectionResult.isInstalled) { - return 'CLI: Not installed'; - } - - const version = versionInfo.version || 'Unknown'; - if (!versionInfo.isSupported) { - return `CLI: ${version} (Outdated)`; - } - - return `CLI: ${version}`; - } catch (_) { - return 'CLI: Error'; - } - } } diff --git a/packages/vscode-ide-companion/src/services/qwenAgentManager.ts b/packages/vscode-ide-companion/src/services/qwenAgentManager.ts index fbd0b530..d7804ab4 100644 --- a/packages/vscode-ide-companion/src/services/qwenAgentManager.ts +++ b/packages/vscode-ide-companion/src/services/qwenAgentManager.ts @@ -284,59 +284,71 @@ export class QwenAgentManager { '[QwenAgentManager] Getting session list with version-aware strategy', ); - try { - console.log( - '[QwenAgentManager] Attempting to get session list via ACP method', - ); - const response = await this.connection.listSessions(); - console.log('[QwenAgentManager] ACP session list response:', response); + // Check if CLI supports session/list method + const cliContextManager = CliContextManager.getInstance(); + const supportsSessionList = cliContextManager.supportsSessionList(); - // sendRequest resolves with the JSON-RPC "result" directly - // Newer CLI returns an object: { items: [...], nextCursor?, hasMore } - // Older prototypes might return an array. Support both. - const res: unknown = response; - let items: Array> = []; + console.log( + '[QwenAgentManager] CLI supports session/list:', + supportsSessionList, + ); - // Note: AcpSessionManager resolves `sendRequest` with the JSON-RPC - // "result" directly (not the full AcpResponse). Treat it as unknown - // and carefully narrow before accessing `items` to satisfy strict TS. - if (res && typeof res === 'object' && 'items' in res) { - const itemsValue = (res as { items?: unknown }).items; - items = Array.isArray(itemsValue) - ? (itemsValue as Array>) - : []; - } + // Try ACP method first if supported + if (supportsSessionList) { + try { + console.log( + '[QwenAgentManager] Attempting to get session list via ACP method', + ); + const response = await this.connection.listSessions(); + console.log('[QwenAgentManager] ACP session list response:', response); - console.log( - '[QwenAgentManager] Sessions retrieved via ACP:', - res, - items.length, - ); - if (items.length > 0) { - const sessions = items.map((item) => ({ - id: item.sessionId || item.id, - sessionId: item.sessionId || item.id, - title: item.title || item.name || item.prompt || 'Untitled Session', - name: item.title || item.name || item.prompt || 'Untitled Session', - startTime: item.startTime, - lastUpdated: item.mtime || item.lastUpdated, - messageCount: item.messageCount || 0, - projectHash: item.projectHash, - filePath: item.filePath, - cwd: item.cwd, - })); + // sendRequest resolves with the JSON-RPC "result" directly + // Newer CLI returns an object: { items: [...], nextCursor?, hasMore } + // Older prototypes might return an array. Support both. + const res: unknown = response; + let items: Array> = []; + + // Note: AcpSessionManager resolves `sendRequest` with the JSON-RPC + // "result" directly (not the full AcpResponse). Treat it as unknown + // and carefully narrow before accessing `items` to satisfy strict TS. + if (res && typeof res === 'object' && 'items' in res) { + const itemsValue = (res as { items?: unknown }).items; + items = Array.isArray(itemsValue) + ? (itemsValue as Array>) + : []; + } console.log( '[QwenAgentManager] Sessions retrieved via ACP:', - sessions.length, + res, + items.length, + ); + if (items.length > 0) { + const sessions = items.map((item) => ({ + id: item.sessionId || item.id, + sessionId: item.sessionId || item.id, + title: item.title || item.name || item.prompt || 'Untitled Session', + name: item.title || item.name || item.prompt || 'Untitled Session', + startTime: item.startTime, + lastUpdated: item.mtime || item.lastUpdated, + messageCount: item.messageCount || 0, + projectHash: item.projectHash, + filePath: item.filePath, + cwd: item.cwd, + })); + + console.log( + '[QwenAgentManager] Sessions retrieved via ACP:', + sessions.length, + ); + return sessions; + } + } catch (error) { + console.warn( + '[QwenAgentManager] ACP session list failed, falling back to file system method:', + error, ); - return sessions; } - } catch (error) { - console.warn( - '[QwenAgentManager] ACP session list failed, falling back to file system method:', - error, - ); } // Always fall back to file system method @@ -393,52 +405,62 @@ export class QwenAgentManager { const size = params?.size ?? 20; const cursor = params?.cursor; - try { - const response = await this.connection.listSessions({ - size, - ...(cursor !== undefined ? { cursor } : {}), - }); - // sendRequest resolves with the JSON-RPC "result" directly - const res: unknown = response; - let items: Array> = []; + const cliContextManager = CliContextManager.getInstance(); + const supportsSessionList = cliContextManager.supportsSessionList(); - if (Array.isArray(res)) { - items = res; - } else if (typeof res === 'object' && res !== null && 'items' in res) { - const responseObject = res as { - items?: Array>; - }; - items = Array.isArray(responseObject.items) ? responseObject.items : []; + if (supportsSessionList) { + try { + const response = await this.connection.listSessions({ + size, + ...(cursor !== undefined ? { cursor } : {}), + }); + // sendRequest resolves with the JSON-RPC "result" directly + const res: unknown = response; + let items: Array> = []; + + if (Array.isArray(res)) { + items = res; + } else if (typeof res === 'object' && res !== null && 'items' in res) { + const responseObject = res as { + items?: Array>; + }; + items = Array.isArray(responseObject.items) + ? responseObject.items + : []; + } + + const mapped = items.map((item) => ({ + id: item.sessionId || item.id, + sessionId: item.sessionId || item.id, + title: item.title || item.name || item.prompt || 'Untitled Session', + name: item.title || item.name || item.prompt || 'Untitled Session', + startTime: item.startTime, + lastUpdated: item.mtime || item.lastUpdated, + messageCount: item.messageCount || 0, + projectHash: item.projectHash, + filePath: item.filePath, + cwd: item.cwd, + })); + + const nextCursor: number | undefined = + typeof res === 'object' && res !== null && 'nextCursor' in res + ? typeof res.nextCursor === 'number' + ? res.nextCursor + : undefined + : undefined; + const hasMore: boolean = + typeof res === 'object' && res !== null && 'hasMore' in res + ? Boolean(res.hasMore) + : false; + + return { sessions: mapped, nextCursor, hasMore }; + } catch (error) { + console.warn( + '[QwenAgentManager] Paged ACP session list failed:', + error, + ); + // fall through to file system } - - const mapped = items.map((item) => ({ - id: item.sessionId || item.id, - sessionId: item.sessionId || item.id, - title: item.title || item.name || item.prompt || 'Untitled Session', - name: item.title || item.name || item.prompt || 'Untitled Session', - startTime: item.startTime, - lastUpdated: item.mtime || item.lastUpdated, - messageCount: item.messageCount || 0, - projectHash: item.projectHash, - filePath: item.filePath, - cwd: item.cwd, - })); - - const nextCursor: number | undefined = - typeof res === 'object' && res !== null && 'nextCursor' in res - ? typeof res.nextCursor === 'number' - ? res.nextCursor - : undefined - : undefined; - const hasMore: boolean = - typeof res === 'object' && res !== null && 'hasMore' in res - ? Boolean(res.hasMore) - : false; - - return { sessions: mapped, nextCursor, hasMore }; - } catch (error) { - console.warn('[QwenAgentManager] Paged ACP session list failed:', error); - // fall through to file system } // Fallback: file system for current project only (to match ACP semantics) @@ -487,28 +509,32 @@ export class QwenAgentManager { */ async getSessionMessages(sessionId: string): Promise { try { - try { - const list = await this.getSessionList(); - const item = list.find( - (s) => s.sessionId === sessionId || s.id === sessionId, - ); - console.log( - '[QwenAgentManager] Session list item for filePath lookup:', - item, - ); - if ( - typeof item === 'object' && - item !== null && - 'filePath' in item && - typeof item.filePath === 'string' - ) { - const messages = await this.readJsonlMessages(item.filePath); - // Even if messages array is empty, we should return it rather than falling back - // This ensures we don't accidentally show messages from a different session format - return messages; + // Prefer reading CLI's JSONL if we can find filePath from session/list + const cliContextManager = CliContextManager.getInstance(); + if (cliContextManager.supportsSessionList()) { + try { + const list = await this.getSessionList(); + const item = list.find( + (s) => s.sessionId === sessionId || s.id === sessionId, + ); + console.log( + '[QwenAgentManager] Session list item for filePath lookup:', + item, + ); + if ( + typeof item === 'object' && + item !== null && + 'filePath' in item && + typeof item.filePath === 'string' + ) { + const messages = await this.readJsonlMessages(item.filePath); + // Even if messages array is empty, we should return it rather than falling back + // This ensures we don't accidentally show messages from a different session format + return messages; + } + } catch (e) { + console.warn('[QwenAgentManager] JSONL read path lookup failed:', e); } - } catch (e) { - console.warn('[QwenAgentManager] JSONL read path lookup failed:', e); } // Fallback: legacy JSON session files diff --git a/packages/vscode-ide-companion/src/utils/authNotificationHandler.ts b/packages/vscode-ide-companion/src/utils/authNotificationHandler.ts index 2fe11e83..3586e042 100644 --- a/packages/vscode-ide-companion/src/utils/authNotificationHandler.ts +++ b/packages/vscode-ide-companion/src/utils/authNotificationHandler.ts @@ -57,13 +57,3 @@ export function handleAuthenticateUpdate( authNotificationDisposable = null; }); } - -/** - * Dismiss the authentication notification if it's currently shown - */ -export function dismissAuthenticateUpdate(): void { - if (authNotificationDisposable) { - authNotificationDisposable.dispose(); - authNotificationDisposable = null; - } -} diff --git a/packages/vscode-ide-companion/src/webview/App.tsx b/packages/vscode-ide-companion/src/webview/App.tsx index b926539d..5eacdabf 100644 --- a/packages/vscode-ide-companion/src/webview/App.tsx +++ b/packages/vscode-ide-companion/src/webview/App.tsx @@ -748,71 +748,74 @@ export const App: React.FC = () => { )} - setIsComposing(true)} - onCompositionEnd={() => setIsComposing(false)} - onKeyDown={() => {}} - onSubmit={handleSubmitWithScroll} - onCancel={handleCancel} - onToggleEditMode={handleToggleEditMode} - onToggleThinking={handleToggleThinking} - onFocusActiveEditor={fileContext.focusActiveEditor} - onToggleSkipAutoActiveContext={() => - setSkipAutoActiveContext((v) => !v) - } - onShowCommandMenu={async () => { - if (inputFieldRef.current) { - inputFieldRef.current.focus(); + {isAuthenticated && ( + setIsComposing(true)} + onCompositionEnd={() => setIsComposing(false)} + onKeyDown={() => {}} + onSubmit={handleSubmitWithScroll} + onCancel={handleCancel} + onToggleEditMode={handleToggleEditMode} + onToggleThinking={handleToggleThinking} + onFocusActiveEditor={fileContext.focusActiveEditor} + onToggleSkipAutoActiveContext={() => + setSkipAutoActiveContext((v) => !v) + } + onShowCommandMenu={async () => { + if (inputFieldRef.current) { + inputFieldRef.current.focus(); - const selection = window.getSelection(); - let position = { top: 0, left: 0 }; + const selection = window.getSelection(); + let position = { top: 0, left: 0 }; - if (selection && selection.rangeCount > 0) { - try { - const range = selection.getRangeAt(0); - const rangeRect = range.getBoundingClientRect(); - if (rangeRect.top > 0 && rangeRect.left > 0) { - position = { - top: rangeRect.top, - left: rangeRect.left, - }; - } else { + if (selection && selection.rangeCount > 0) { + try { + const range = selection.getRangeAt(0); + const rangeRect = range.getBoundingClientRect(); + if (rangeRect.top > 0 && rangeRect.left > 0) { + position = { + top: rangeRect.top, + left: rangeRect.left, + }; + } else { + const inputRect = + inputFieldRef.current.getBoundingClientRect(); + position = { top: inputRect.top, left: inputRect.left }; + } + } catch (error) { + console.error('[App] Error getting cursor position:', error); const inputRect = inputFieldRef.current.getBoundingClientRect(); position = { top: inputRect.top, left: inputRect.left }; } - } catch (error) { - console.error('[App] Error getting cursor position:', error); + } else { const inputRect = inputFieldRef.current.getBoundingClientRect(); position = { top: inputRect.top, left: inputRect.left }; } - } else { - const inputRect = inputFieldRef.current.getBoundingClientRect(); - position = { top: inputRect.top, left: inputRect.left }; + + await completion.openCompletion('/', '', position); } + }} + onAttachContext={handleAttachContextClick} + completionIsOpen={completion.isOpen} + completionItems={completion.items} + onCompletionSelect={handleCompletionSelect} + onCompletionClose={completion.closeCompletion} + /> + )} - await completion.openCompletion('/', '', position); - } - }} - onAttachContext={handleAttachContextClick} - completionIsOpen={completion.isOpen} - completionItems={completion.items} - onCompletionSelect={handleCompletionSelect} - onCompletionClose={completion.closeCompletion} - /> - - {permissionRequest && ( + {isAuthenticated && permissionRequest && ( - Resolves when auth state restoration attempt is complete */ private async attemptAuthStateRestoration(): Promise { try { - console.log( - '[WebViewProvider] Attempting connection (without auto-auth)...', - ); - // Attempt a lightweight connection to detect prior auth without forcing login + console.log('[WebViewProvider] Attempting connection...'); + // Attempt a connection to detect prior auth without forcing login await this.initializeAgentConnection({ autoAuthenticate: false }); } catch (error) { console.error( @@ -570,16 +548,6 @@ export class WebViewProvider { /** * Internal: perform actual connection/initialization (no auth locking). - * - * This method handles the complete agent connection and initialization workflow: - * 1. Detects if Qwen CLI is installed - * 2. If CLI is not installed, prompts user for installation - * 3. If CLI is installed, attempts to connect to the agent - * 4. Handles authentication requirements and session creation - * 5. Notifies WebView of connection status - * - * @param options - Connection options including auto-authentication setting - * @returns Promise - Resolves when initialization is complete */ private async doInitializeAgentConnection(options?: { autoAuthenticate?: boolean; @@ -623,18 +591,7 @@ export class WebViewProvider { // Perform version check with throttled notifications const versionChecker = CliVersionChecker.getInstance(this.context); - const versionCheckResult = await versionChecker.checkCliVersion(false); // Silent check to avoid popup spam - - if (!versionCheckResult.isSupported) { - console.log( - '[WebViewProvider] Qwen CLI version is outdated or unsupported', - versionCheckResult, - ); - // Log to output channel instead of showing popup - console.warn( - `Qwen Code CLI version issue: Installed=${versionCheckResult.version || 'unknown'}, Supported=${versionCheckResult.isSupported}`, - ); - } + await versionChecker.checkCliVersion(true); // Silent check to avoid popup spam try { console.log('[WebViewProvider] Connecting to agent...'); @@ -674,9 +631,6 @@ export class WebViewProvider { const sessionReady = await this.loadCurrentSessionMessages(options); if (sessionReady) { - // Dismiss any authentication notifications - dismissAuthenticateUpdate(); - // Notify webview that agent is connected this.sendMessageToWebView({ type: 'agentConnected', @@ -751,9 +705,6 @@ export class WebViewProvider { '[WebViewProvider] Force re-login completed successfully', ); - // Dismiss any authentication notifications - dismissAuthenticateUpdate(); - // Send success notification to WebView this.sendMessageToWebView({ type: 'loginSuccess', @@ -808,9 +759,6 @@ export class WebViewProvider { '[WebViewProvider] Connection refresh completed successfully', ); - // Dismiss any authentication notifications - dismissAuthenticateUpdate(); - // Notify webview that agent is connected after refresh this.sendMessageToWebView({ type: 'agentConnected', diff --git a/packages/vscode-ide-companion/src/webview/components/layout/Onboarding.tsx b/packages/vscode-ide-companion/src/webview/components/layout/Onboarding.tsx index f4c8679c..2eddc4d3 100644 --- a/packages/vscode-ide-companion/src/webview/components/layout/Onboarding.tsx +++ b/packages/vscode-ide-companion/src/webview/components/layout/Onboarding.tsx @@ -15,7 +15,7 @@ export const Onboarding: React.FC = ({ onLogin }) => { return (
-
+
{/* Application icon container */}