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 */}