From 0ac191e2db511fedfc7a8a3076f38c4ea7bf436d Mon Sep 17 00:00:00 2001 From: yiliang114 <1204183885@qq.com> Date: Sat, 13 Dec 2025 17:50:15 +0800 Subject: [PATCH] chore(vscode-ide-companion): wip --- .../src/cli/cliDetector.ts | 13 - .../src/cli/cliVersionChecker.ts | 229 +++++++++------- .../src/extension.test.ts | 12 + .../src/services/qwenAgentManager.ts | 250 ++++++++---------- .../src/services/qwenConnectionHandler.ts | 10 +- .../src/utils/authNotificationHandler.ts | 65 +++-- .../vscode-ide-companion/src/webview/App.tsx | 25 +- .../src/webview/WebViewProvider.ts | 44 ++- .../webview/components/layout/EmptyState.tsx | 39 +-- 9 files changed, 393 insertions(+), 294 deletions(-) diff --git a/packages/vscode-ide-companion/src/cli/cliDetector.ts b/packages/vscode-ide-companion/src/cli/cliDetector.ts index b4ec3df81..20570c742 100644 --- a/packages/vscode-ide-companion/src/cli/cliDetector.ts +++ b/packages/vscode-ide-companion/src/cli/cliDetector.ts @@ -50,15 +50,9 @@ export class CliDetector { this.cachedResult && now - this.lastCheckTime < this.CACHE_DURATION_MS ) { - console.log('[CliDetector] Returning cached result'); return this.cachedResult; } - console.log( - '[CliDetector] Starting lightweight CLI detection, current PATH:', - process.env.PATH, - ); - try { const isWindows = process.platform === 'win32'; const whichCommand = isWindows ? 'where' : 'which'; @@ -70,11 +64,6 @@ export class CliDetector { ? `${whichCommand} qwen` : `${whichCommand} qwen`; - console.log( - '[CliDetector] Detecting CLI with lightweight command:', - detectionCommand, - ); - // Execute command to detect CLI path, set shorter timeout (3 seconds) const { stdout } = await execAsync(detectionCommand, { timeout: 3000, // Reduced timeout for faster detection @@ -88,8 +77,6 @@ export class CliDetector { .filter((line) => line.trim()); const cliPath = lines[0]; // Take only the first path - console.log('[CliDetector] Found CLI at:', cliPath); - // Build successful detection result, note no version information this.cachedResult = { isInstalled: true, diff --git a/packages/vscode-ide-companion/src/cli/cliVersionChecker.ts b/packages/vscode-ide-companion/src/cli/cliVersionChecker.ts index 9bf238d71..b5ffaaa67 100644 --- a/packages/vscode-ide-companion/src/cli/cliVersionChecker.ts +++ b/packages/vscode-ide-companion/src/cli/cliVersionChecker.ts @@ -5,121 +5,154 @@ */ import * as vscode from 'vscode'; -import { CliContextManager } from './cliContextManager.js'; +import { CliDetector, type CliDetectionResult } from './cliDetector.js'; import { CliVersionManager } from './cliVersionManager.js'; -import { MIN_CLI_VERSION_FOR_SESSION_METHODS } from './cliVersionManager.js'; -import type { CliVersionInfo } from './cliVersionManager.js'; - -// Track which versions have already been warned about to avoid repetitive warnings -// Using a Map with timestamps to allow warnings to be shown again after a certain period -const warnedVersions = new Map(); -const WARNING_COOLDOWN_MS = 24 * 60 * 60 * 1000; // 24 hours cooldown +import semver from 'semver'; /** - * Check CLI version and show warning if below minimum requirement - * Provides an "Upgrade Now" option for unsupported versions + * CLI Version Checker * - * @returns Version information + * Handles CLI version checking with throttling to prevent frequent notifications. + * This class manages version checking and provides version information without + * constantly bothering the user with popups. */ -export async function checkCliVersionAndWarn(): Promise { - try { - const cliContextManager = CliContextManager.getInstance(); - const versionInfo = - await CliVersionManager.getInstance().detectCliVersion(true); - cliContextManager.setCurrentVersionInfo(versionInfo); +export class CliVersionChecker { + private static instance: CliVersionChecker; + private lastNotificationTime: number = 0; + private static readonly NOTIFICATION_COOLDOWN_MS = 300000; // 5 minutes cooldown + private context: vscode.ExtensionContext; - if (!versionInfo.isSupported) { - // Only show warning if we haven't already warned about this specific version recently - const versionKey = versionInfo.version || 'unknown'; - const lastWarningTime = warnedVersions.get(versionKey); - const currentTime = Date.now(); + private constructor(context: vscode.ExtensionContext) { + this.context = context; + } - // Show warning if we haven't warned about this version or if enough time has passed - if ( - !lastWarningTime || - currentTime - lastWarningTime > WARNING_COOLDOWN_MS - ) { - // Wait to determine release version number - const selection = await vscode.window.showWarningMessage( - `Qwen Code CLI version ${versionInfo.version} is below the minimum required version. Some features may not work properly. Please upgrade to version ${MIN_CLI_VERSION_FOR_SESSION_METHODS} or later.`, - 'Upgrade Now', - ); + /** + * Get singleton instance + */ + static getInstance(context?: vscode.ExtensionContext): CliVersionChecker { + if (!CliVersionChecker.instance && context) { + CliVersionChecker.instance = new CliVersionChecker(context); + } + return CliVersionChecker.instance; + } - // Handle the user's selection - if (selection === 'Upgrade Now') { - // Open terminal and run npm install command - const terminal = vscode.window.createTerminal( - 'Qwen Code CLI Upgrade', + /** + * Check CLI version with cooldown to prevent spamming notifications + * + * @param showNotifications - Whether to show notifications for issues + * @returns Promise with version check result + */ + async checkCliVersion(showNotifications: boolean = true): Promise<{ + isInstalled: boolean; + version?: string; + isSupported: boolean; + needsUpdate: boolean; + error?: string; + }> { + try { + // Detect CLI installation + const detectionResult: CliDetectionResult = + await CliDetector.detectQwenCli(); + + if (!detectionResult.isInstalled) { + if (showNotifications && this.canShowNotification()) { + vscode.window.showWarningMessage( + `Qwen Code CLI not found. Please install it using: npm install -g @qwen-code/qwen-code@latest`, ); - terminal.show(); - terminal.sendText('npm install -g @qwen-code/qwen-code@latest'); + this.lastNotificationTime = Date.now(); } - // Update the last warning time - warnedVersions.set(versionKey, currentTime); + return { + isInstalled: false, + error: detectionResult.error, + isSupported: false, + needsUpdate: false, + }; } - } - return versionInfo; - } catch (error) { - console.error('[CliVersionChecker] Failed to check CLI version:', error); - // Return a default version info in case of error - return { - version: undefined, - isSupported: false, - features: { - supportsSessionList: false, - supportsSessionLoad: false, - }, - detectionResult: { + // Get version information + const versionManager = CliVersionManager.getInstance(); + const versionInfo = await versionManager.detectCliVersion(); + + const currentVersion = detectionResult.version; + 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}`) + : 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`, + ); + this.lastNotificationTime = Date.now(); + } + + return { + isInstalled: true, + version: currentVersion, + isSupported, + needsUpdate, + }; + } catch (error) { + console.error('[CliVersionChecker] Version check failed:', error); + + if (showNotifications && this.canShowNotification()) { + vscode.window.showErrorMessage( + `Failed to check Qwen Code CLI version: ${error instanceof Error ? error.message : String(error)}`, + ); + this.lastNotificationTime = Date.now(); + } + + return { isInstalled: false, error: error instanceof Error ? error.message : String(error), - }, - }; - } -} - -/** - * Process server version information from initialize response - * - * @param init - Initialize response object - */ -export function processServerVersion(init: unknown): void { - try { - const obj = (init || {}) as Record; - - // Extract version information from initialize response - const serverVersion = - obj['version'] || obj['serverVersion'] || obj['cliVersion']; - if (serverVersion) { - console.log( - '[CliVersionChecker] Server version from initialize response:', - serverVersion, - ); - - // Update CLI context with version info from server - const cliContextManager = CliContextManager.getInstance(); - - // Create version info directly without async call - const versionInfo: CliVersionInfo = { - version: String(serverVersion), - isSupported: true, // Assume supported for now - features: { - supportsSessionList: true, - supportsSessionLoad: true, - }, - detectionResult: { - isInstalled: true, - version: String(serverVersion), - }, + isSupported: false, + needsUpdate: false, }; - - cliContextManager.setCurrentVersionInfo(versionInfo); } - } catch (error) { - console.error( - '[CliVersionChecker] Failed to process server version:', - error, + } + + /** + * Check if notification can be shown based on cooldown period + */ + private canShowNotification(): boolean { + return ( + Date.now() - this.lastNotificationTime > + 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/extension.test.ts b/packages/vscode-ide-companion/src/extension.test.ts index 31d5aa52f..dd6b3352a 100644 --- a/packages/vscode-ide-companion/src/extension.test.ts +++ b/packages/vscode-ide-companion/src/extension.test.ts @@ -43,6 +43,14 @@ vi.mock('vscode', () => ({ registerWebviewPanelSerializer: vi.fn(() => ({ dispose: vi.fn(), })), + createStatusBarItem: vi.fn(() => ({ + text: '', + tooltip: '', + command: '', + show: vi.fn(), + hide: vi.fn(), + dispose: vi.fn(), + })), }, workspace: { workspaceFolders: [], @@ -58,6 +66,10 @@ vi.mock('vscode', () => ({ Uri: { joinPath: vi.fn(), }, + StatusBarAlignment: { + Left: 1, + Right: 2, + }, ExtensionMode: { Development: 1, Production: 2, diff --git a/packages/vscode-ide-companion/src/services/qwenAgentManager.ts b/packages/vscode-ide-companion/src/services/qwenAgentManager.ts index 97fffe157..fbd0b5302 100644 --- a/packages/vscode-ide-companion/src/services/qwenAgentManager.ts +++ b/packages/vscode-ide-companion/src/services/qwenAgentManager.ts @@ -26,7 +26,6 @@ import { QwenSessionUpdateHandler } from './qwenSessionUpdateHandler.js'; import { CliContextManager } from '../cli/cliContextManager.js'; import { authMethod } from '../types/acpTypes.js'; import { MIN_CLI_VERSION_FOR_SESSION_METHODS } from '../cli/cliVersionManager.js'; -import { processServerVersion } from '../cli/cliVersionChecker.js'; import { isAuthenticationRequiredError } from '../utils/authErrors.js'; import { handleAuthenticateUpdate } from '../utils/authNotificationHandler.js'; @@ -163,9 +162,6 @@ export class QwenAgentManager { // Initialize callback to surface available modes and current mode to UI this.connection.onInitialized = (init: unknown) => { try { - // Process server version information - processServerVersion(init); - const obj = (init || {}) as Record; const modes = obj['modes'] as | { @@ -288,71 +284,59 @@ export class QwenAgentManager { '[QwenAgentManager] Getting session list with version-aware strategy', ); - // Check if CLI supports session/list method - const cliContextManager = CliContextManager.getInstance(); - const supportsSessionList = cliContextManager.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] CLI supports session/list:', - 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> = []; - // 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); + // 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>) + : []; + } - // 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:', + 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:', - 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, + sessions.length, ); + 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 @@ -409,62 +393,52 @@ export class QwenAgentManager { const size = params?.size ?? 20; const cursor = params?.cursor; - const cliContextManager = CliContextManager.getInstance(); - const supportsSessionList = cliContextManager.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 (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 + 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 } // Fallback: file system for current project only (to match ACP semantics) @@ -513,32 +487,28 @@ export class QwenAgentManager { */ async getSessionMessages(sessionId: string): Promise { try { - // 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); + 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); } // Fallback: legacy JSON session files diff --git a/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts b/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts index 2fbdd4061..328738079 100644 --- a/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts +++ b/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts @@ -15,7 +15,6 @@ import type { QwenSessionReader } from '../services/qwenSessionReader.js'; import { CliDetector } from '../cli/cliDetector.js'; import { authMethod } from '../types/acpTypes.js'; import { isAuthenticationRequiredError } from '../utils/authErrors.js'; -import { checkCliVersionAndWarn } from '../cli/cliVersionChecker.js'; export interface QwenConnectionResult { sessionCreated: boolean; @@ -50,18 +49,15 @@ export class QwenConnectionHandler { let sessionCreated = false; let requiresAuth = false; - // Lightweight check if CLI exists (without version info for faster performance) - const detectionResult = await CliDetector.detectQwenCliLightweight( - /* forceRefresh */ true, + // Check if CLI exists using standard detection (with cached results for better performance) + const detectionResult = await CliDetector.detectQwenCli( + /* forceRefresh */ false, // Use cached results when available for better performance ); if (!detectionResult.isInstalled) { throw new Error(detectionResult.error || 'Qwen CLI not found'); } console.log('[QwenAgentManager] CLI detected at:', detectionResult.cliPath); - // Show warning if CLI version is below minimum requirement - await checkCliVersionAndWarn(); - // Build extra CLI arguments (only essential parameters) const extraArgs: string[] = []; diff --git a/packages/vscode-ide-companion/src/utils/authNotificationHandler.ts b/packages/vscode-ide-companion/src/utils/authNotificationHandler.ts index 8f707f4df..2fe11e834 100644 --- a/packages/vscode-ide-companion/src/utils/authNotificationHandler.ts +++ b/packages/vscode-ide-companion/src/utils/authNotificationHandler.ts @@ -7,6 +7,9 @@ import * as vscode from 'vscode'; import type { AuthenticateUpdateNotification } from '../types/acpTypes.js'; +// Store reference to the authentication notification to allow auto-closing +let authNotificationDisposable: { dispose: () => void } | null = null; + /** * Handle authentication update notifications by showing a VS Code notification * with the authentication URI and a copy button. @@ -18,23 +21,49 @@ export function handleAuthenticateUpdate( ): void { const authUri = data._meta.authUri; + // Dismiss any existing authentication notification + if (authNotificationDisposable) { + authNotificationDisposable.dispose(); + authNotificationDisposable = null; + } + // Show an information message with the auth URI and copy button - vscode.window - .showInformationMessage( - `Qwen Code needs authentication. Click the button below to open the authentication page or copy the link to your browser.`, - 'Open in Browser', - 'Copy Link', - ) - .then((selection) => { - if (selection === 'Open in Browser') { - // Open the authentication URI in the default browser - vscode.env.openExternal(vscode.Uri.parse(authUri)); - } else if (selection === 'Copy Link') { - // Copy the authentication URI to clipboard - vscode.env.clipboard.writeText(authUri); - vscode.window.showInformationMessage( - 'Authentication link copied to clipboard!', - ); - } - }); + const notificationPromise = vscode.window.showInformationMessage( + `Qwen Code needs authentication. Click the button below to open the authentication page or copy the link to your browser.`, + 'Open in Browser', + 'Copy Link', + ); + + // Create a simple disposable object + authNotificationDisposable = { + dispose: () => { + // We can't actually cancel the promise, but we can clear our reference + }, + }; + + notificationPromise.then((selection) => { + if (selection === 'Open in Browser') { + // Open the authentication URI in the default browser + vscode.env.openExternal(vscode.Uri.parse(authUri)); + } else if (selection === 'Copy Link') { + // Copy the authentication URI to clipboard + vscode.env.clipboard.writeText(authUri); + vscode.window.showInformationMessage( + 'Authentication link copied to clipboard!', + ); + } + + // Clear the notification reference after user interaction + 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 39ba6ff90..b926539da 100644 --- a/packages/vscode-ide-companion/src/webview/App.tsx +++ b/packages/vscode-ide-companion/src/webview/App.tsx @@ -69,6 +69,7 @@ export const App: React.FC = () => { } | null>(null); const [planEntries, setPlanEntries] = useState([]); const [isAuthenticated, setIsAuthenticated] = useState(null); + const [isLoading, setIsLoading] = useState(true); // Track if we're still initializing/loading const messagesEndRef = useRef( null, ) as React.RefObject; @@ -360,6 +361,14 @@ export const App: React.FC = () => { completedToolCalls, ]); + // Set loading state to false after initial mount and when we have authentication info + useEffect(() => { + // If we have determined authentication status, we're done loading + if (isAuthenticated !== null) { + setIsLoading(false); + } + }, [isAuthenticated]); + // Handle permission response const handlePermissionResponse = useCallback( (optionId: string) => { @@ -666,7 +675,19 @@ export const App: React.FC = () => { allMessages.length > 0; return ( -
+
+ {/* Top-level loading overlay */} + {isLoading && ( +
+
+
+

+ Preparing Qwen Code... +

+
+
+ )} + { ref={messagesContainerRef} className="chat-messages messages-container flex-1 overflow-y-auto overflow-x-hidden pt-5 pr-5 pl-5 pb-[140px] flex flex-col relative min-w-0 focus:outline-none [&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar-track]:bg-transparent [&::-webkit-scrollbar-thumb]:bg-white/20 [&::-webkit-scrollbar-thumb]:rounded-sm [&::-webkit-scrollbar-thumb]:hover:bg-white/30 [&>*]:flex [&>*]:gap-0 [&>*]:items-start [&>*]:text-left [&>*]:py-2 [&>*:not(:last-child)]:pb-[8px] [&>*]:flex-col [&>*]:relative [&>*]:animate-[fadeIn_0.2s_ease-in]" > - {!hasContent ? ( + {!hasContent && !isLoading ? ( isAuthenticated === false ? ( { diff --git a/packages/vscode-ide-companion/src/webview/WebViewProvider.ts b/packages/vscode-ide-companion/src/webview/WebViewProvider.ts index d06c92434..c45ac6af8 100644 --- a/packages/vscode-ide-companion/src/webview/WebViewProvider.ts +++ b/packages/vscode-ide-companion/src/webview/WebViewProvider.ts @@ -13,9 +13,11 @@ import { PanelManager } from '../webview/PanelManager.js'; import { MessageHandler } from '../webview/MessageHandler.js'; import { WebViewContent } from '../webview/WebViewContent.js'; import { CliInstaller } from '../cli/cliInstaller.js'; +import { CliVersionChecker } from '../cli/cliVersionChecker.js'; import { getFileName } from './utils/webviewUtils.js'; import { type ApprovalModeValue } from '../types/approvalModeValueTypes.js'; import { isAuthenticationRequiredError } from '../utils/authErrors.js'; +import { dismissAuthenticateUpdate } from '../utils/authNotificationHandler.js'; /** * WebView Provider Class @@ -46,7 +48,7 @@ export class WebViewProvider { private currentModeId: ApprovalModeValue | null = null; constructor( - context: vscode.ExtensionContext, + private context: vscode.ExtensionContext, private extensionUri: vscode.Uri, ) { this.agentManager = new QwenAgentManager(); @@ -619,6 +621,21 @@ export class WebViewProvider { console.log('[WebViewProvider] CLI path:', cliDetection.cliPath); console.log('[WebViewProvider] CLI version:', cliDetection.version); + // 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}`, + ); + } + try { console.log('[WebViewProvider] Connecting to agent...'); @@ -630,6 +647,22 @@ export class WebViewProvider { ); console.log('[WebViewProvider] Agent connected successfully'); this.agentInitialized = true; + + // If authentication is required and autoAuthenticate is false, + // send authState message and return without creating session + if (connectResult.requiresAuth && !autoAuthenticate) { + console.log( + '[WebViewProvider] Authentication required but auto-auth disabled, sending authState and returning', + ); + this.sendMessageToWebView({ + type: 'authState', + data: { authenticated: false }, + }); + // Initialize empty conversation to allow browsing history + await this.initializeEmptyConversation(); + return; + } + if (connectResult.requiresAuth) { this.sendMessageToWebView({ type: 'authState', @@ -641,6 +674,9 @@ 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', @@ -715,6 +751,9 @@ export class WebViewProvider { '[WebViewProvider] Force re-login completed successfully', ); + // Dismiss any authentication notifications + dismissAuthenticateUpdate(); + // Send success notification to WebView this.sendMessageToWebView({ type: 'loginSuccess', @@ -769,6 +808,9 @@ 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/EmptyState.tsx b/packages/vscode-ide-companion/src/webview/components/layout/EmptyState.tsx index 4c4a486ed..1b424e249 100644 --- a/packages/vscode-ide-companion/src/webview/components/layout/EmptyState.tsx +++ b/packages/vscode-ide-companion/src/webview/components/layout/EmptyState.tsx @@ -27,24 +27,33 @@ export const EmptyState: React.FC = ({ return (
- {/* Loading overlay */} - {loadingMessage && ( -
-
-
-

{loadingMessage}

-
-
- )} -
{/* Qwen Logo */}
- Qwen Logo + {iconUri ? ( + Qwen Logo { + // Fallback to a div with text if image fails to load + const target = e.target as HTMLImageElement; + target.style.display = 'none'; + const parent = target.parentElement; + if (parent) { + const fallback = document.createElement('div'); + fallback.className = + 'w-[60px] h-[60px] flex items-center justify-center text-2xl font-bold'; + fallback.textContent = 'Q'; + parent.appendChild(fallback); + } + }} + /> + ) : ( +
+ Q +
+ )}
{description}