From f5306339f69d400b171a7d1d82902350dac469c7 Mon Sep 17 00:00:00 2001 From: yiliang114 <1204183885@qq.com> Date: Sat, 13 Dec 2025 00:01:05 +0800 Subject: [PATCH 01/18] refactor(vscode-ide-companion/types): move ApprovalModeValue type to dedicated file feat(vscode-ide-companion/file-context): improve file context handling and search Enhance file context hook to better handle search queries and reduce redundant requests. Track last query to optimize when to refetch full file list. Improve logging for debugging purposes. --- .../src/services/acpConnection.ts | 2 +- .../src/services/acpSessionManager.ts | 2 +- .../src/services/qwenAgentManager.ts | 2 +- .../src/services/qwenSessionUpdateHandler.ts | 3 +- .../src/types/acpTypes.ts | 3 +- .../src/types/approvalModeValueTypes.ts | 11 ++++++ .../src/types/chatTypes.ts | 3 +- .../vscode-ide-companion/src/webview/App.tsx | 39 +++++++++++++++---- .../src/webview/WebViewProvider.ts | 2 +- .../webview/components/layout/InputForm.tsx | 2 +- .../webview/handlers/SessionMessageHandler.ts | 30 ++++++++++++++ .../src/webview/hooks/file/useFileContext.ts | 28 ++++++++----- .../src/webview/hooks/useWebViewMessages.ts | 2 +- 13 files changed, 103 insertions(+), 26 deletions(-) create mode 100644 packages/vscode-ide-companion/src/types/approvalModeValueTypes.ts diff --git a/packages/vscode-ide-companion/src/services/acpConnection.ts b/packages/vscode-ide-companion/src/services/acpConnection.ts index f4c95948..464f8bcb 100644 --- a/packages/vscode-ide-companion/src/services/acpConnection.ts +++ b/packages/vscode-ide-companion/src/services/acpConnection.ts @@ -10,8 +10,8 @@ import type { AcpPermissionRequest, AcpResponse, AcpSessionUpdate, - ApprovalModeValue, } from '../types/acpTypes.js'; +import type { ApprovalModeValue } from '../types/approvalModeValueTypes.js'; import type { ChildProcess, SpawnOptions } from 'child_process'; import { spawn } from 'child_process'; import type { diff --git a/packages/vscode-ide-companion/src/services/acpSessionManager.ts b/packages/vscode-ide-companion/src/services/acpSessionManager.ts index 8812282a..55b1d2b5 100644 --- a/packages/vscode-ide-companion/src/services/acpSessionManager.ts +++ b/packages/vscode-ide-companion/src/services/acpSessionManager.ts @@ -14,8 +14,8 @@ import type { AcpRequest, AcpNotification, AcpResponse, - ApprovalModeValue, } from '../types/acpTypes.js'; +import type { ApprovalModeValue } from '../types/approvalModeValueTypes.js'; import { AGENT_METHODS } from '../constants/acpSchema.js'; import type { PendingRequest } from '../types/connectionTypes.js'; import type { ChildProcess } from 'child_process'; diff --git a/packages/vscode-ide-companion/src/services/qwenAgentManager.ts b/packages/vscode-ide-companion/src/services/qwenAgentManager.ts index 5ddd5612..c3aa6525 100644 --- a/packages/vscode-ide-companion/src/services/qwenAgentManager.ts +++ b/packages/vscode-ide-companion/src/services/qwenAgentManager.ts @@ -7,8 +7,8 @@ import { AcpConnection } from './acpConnection.js'; import type { AcpSessionUpdate, AcpPermissionRequest, - ApprovalModeValue, } from '../types/acpTypes.js'; +import type { ApprovalModeValue } from '../types/approvalModeValueTypes.js'; import { QwenSessionReader, type QwenSession } from './qwenSessionReader.js'; import { QwenSessionManager } from './qwenSessionManager.js'; import type { diff --git a/packages/vscode-ide-companion/src/services/qwenSessionUpdateHandler.ts b/packages/vscode-ide-companion/src/services/qwenSessionUpdateHandler.ts index e27fbe67..d7b24bb2 100644 --- a/packages/vscode-ide-companion/src/services/qwenSessionUpdateHandler.ts +++ b/packages/vscode-ide-companion/src/services/qwenSessionUpdateHandler.ts @@ -10,7 +10,8 @@ * Handles session updates from ACP and dispatches them to appropriate callbacks */ -import type { AcpSessionUpdate, ApprovalModeValue } from '../types/acpTypes.js'; +import type { AcpSessionUpdate } from '../types/acpTypes.js'; +import type { ApprovalModeValue } from '../types/approvalModeValueTypes.js'; import type { QwenAgentCallbacks } from '../types/chatTypes.js'; /** diff --git a/packages/vscode-ide-companion/src/types/acpTypes.ts b/packages/vscode-ide-companion/src/types/acpTypes.ts index 1fb4de17..252f3d5d 100644 --- a/packages/vscode-ide-companion/src/types/acpTypes.ts +++ b/packages/vscode-ide-companion/src/types/acpTypes.ts @@ -3,6 +3,7 @@ * Copyright 2025 Qwen Team * SPDX-License-Identifier: Apache-2.0 */ +import type { ApprovalModeValue } from './approvalModeValueTypes.js'; export const JSONRPC_VERSION = '2.0' as const; export const authMethod = 'qwen-oauth'; @@ -138,8 +139,6 @@ export interface PlanUpdate extends BaseSessionUpdate { }; } -export type ApprovalModeValue = 'plan' | 'default' | 'auto-edit' | 'yolo'; - export { ApprovalMode, APPROVAL_MODE_MAP, diff --git a/packages/vscode-ide-companion/src/types/approvalModeValueTypes.ts b/packages/vscode-ide-companion/src/types/approvalModeValueTypes.ts new file mode 100644 index 00000000..fe1f37e1 --- /dev/null +++ b/packages/vscode-ide-companion/src/types/approvalModeValueTypes.ts @@ -0,0 +1,11 @@ +/** + * @license + * Copyright 2025 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Type for approval mode values + * Used in ACP protocol for controlling agent behavior + */ +export type ApprovalModeValue = 'plan' | 'default' | 'auto-edit' | 'yolo'; diff --git a/packages/vscode-ide-companion/src/types/chatTypes.ts b/packages/vscode-ide-companion/src/types/chatTypes.ts index 90ebbb87..bafe154d 100644 --- a/packages/vscode-ide-companion/src/types/chatTypes.ts +++ b/packages/vscode-ide-companion/src/types/chatTypes.ts @@ -3,7 +3,8 @@ * Copyright 2025 Qwen Team * SPDX-License-Identifier: Apache-2.0 */ -import type { AcpPermissionRequest, ApprovalModeValue } from './acpTypes.js'; +import type { AcpPermissionRequest } from './acpTypes.js'; +import type { ApprovalModeValue } from './approvalModeValueTypes.js'; export interface ChatMessage { role: 'user' | 'assistant'; diff --git a/packages/vscode-ide-companion/src/webview/App.tsx b/packages/vscode-ide-companion/src/webview/App.tsx index 4bdf6622..1db91d39 100644 --- a/packages/vscode-ide-companion/src/webview/App.tsx +++ b/packages/vscode-ide-companion/src/webview/App.tsx @@ -43,7 +43,7 @@ import { InputForm } from './components/layout/InputForm.js'; import { SessionSelector } from './components/layout/SessionSelector.js'; import { FileIcon, UserIcon } from './components/icons/index.js'; import { ApprovalMode, NEXT_APPROVAL_MODE } from '../types/acpTypes.js'; -import type { ApprovalModeValue } from '../types/acpTypes.js'; +import type { ApprovalModeValue } from '../types/approvalModeValueTypes.js'; import type { PlanEntry } from '../types/chatTypes.js'; export const App: React.FC = () => { @@ -90,9 +90,13 @@ export const App: React.FC = () => { const getCompletionItems = React.useCallback( async (trigger: '@' | '/', query: string): Promise => { if (trigger === '@') { - if (!fileContext.hasRequestedFiles) { - fileContext.requestWorkspaceFiles(); - } + console.log('[App] getCompletionItems @ called', { + query, + requested: fileContext.hasRequestedFiles, + workspaceFiles: fileContext.workspaceFiles.length, + }); + // 始终根据当前 query 触发请求,让 hook 判断是否需要真正请求 + fileContext.requestWorkspaceFiles(query); const fileIcon = ; const allItems: CompletionItem[] = fileContext.workspaceFiles.map( @@ -109,7 +113,6 @@ export const App: React.FC = () => { ); if (query && query.length >= 1) { - fileContext.requestWorkspaceFiles(query); const lowerQuery = query.toLowerCase(); return allItems.filter( (item) => @@ -154,17 +157,39 @@ export const App: React.FC = () => { const completion = useCompletionTrigger(inputFieldRef, getCompletionItems); + // Track a lightweight signature of workspace files to detect content changes even when length is unchanged + const workspaceFilesSignature = useMemo( + () => + fileContext.workspaceFiles + .map( + (file) => + `${file.id}|${file.label}|${file.description ?? ''}|${file.path}`, + ) + .join('||'), + [fileContext.workspaceFiles], + ); + // When workspace files update while menu open for @, refresh items so the first @ shows the list // Note: Avoid depending on the entire `completion` object here, since its identity // changes on every render which would retrigger this effect and can cause a refresh loop. useEffect(() => { - if (completion.isOpen && completion.triggerChar === '@') { + // Only auto-refresh when there's no query (first @ popup) to avoid repeated refreshes during search + if ( + completion.isOpen && + completion.triggerChar === '@' && + !completion.query + ) { // Only refresh items; do not change other completion state to avoid re-renders loops completion.refreshCompletion(); } // Only re-run when the actual data source changes, not on every render // eslint-disable-next-line react-hooks/exhaustive-deps - }, [fileContext.workspaceFiles, completion.isOpen, completion.triggerChar]); + }, [ + workspaceFilesSignature, + completion.isOpen, + completion.triggerChar, + completion.query, + ]); // Message submission const { handleSubmit: submitMessage } = useMessageSubmit({ diff --git a/packages/vscode-ide-companion/src/webview/WebViewProvider.ts b/packages/vscode-ide-companion/src/webview/WebViewProvider.ts index b4da60ab..f2b36ab0 100644 --- a/packages/vscode-ide-companion/src/webview/WebViewProvider.ts +++ b/packages/vscode-ide-companion/src/webview/WebViewProvider.ts @@ -14,7 +14,7 @@ import { MessageHandler } from '../webview/MessageHandler.js'; import { WebViewContent } from '../webview/WebViewContent.js'; import { CliInstaller } from '../cli/cliInstaller.js'; import { getFileName } from './utils/webviewUtils.js'; -import { type ApprovalModeValue } from '../types/acpTypes.js'; +import { type ApprovalModeValue } from '../types/approvalModeValueTypes.js'; export class WebViewProvider { private panelManager: PanelManager; diff --git a/packages/vscode-ide-companion/src/webview/components/layout/InputForm.tsx b/packages/vscode-ide-companion/src/webview/components/layout/InputForm.tsx index fe86ea99..5c4a889a 100644 --- a/packages/vscode-ide-companion/src/webview/components/layout/InputForm.tsx +++ b/packages/vscode-ide-companion/src/webview/components/layout/InputForm.tsx @@ -20,7 +20,7 @@ import { import { CompletionMenu } from '../layout/CompletionMenu.js'; import type { CompletionItem } from '../../../types/completionItemTypes.js'; import { getApprovalModeInfoFromString } from '../../../types/acpTypes.js'; -import type { ApprovalModeValue } from '../../../types/acpTypes.js'; +import type { ApprovalModeValue } from '../../../types/approvalModeValueTypes.js'; interface InputFormProps { inputText: string; diff --git a/packages/vscode-ide-companion/src/webview/handlers/SessionMessageHandler.ts b/packages/vscode-ide-companion/src/webview/handlers/SessionMessageHandler.ts index 0df3e0da..75ebe0b9 100644 --- a/packages/vscode-ide-companion/src/webview/handlers/SessionMessageHandler.ts +++ b/packages/vscode-ide-companion/src/webview/handlers/SessionMessageHandler.ts @@ -7,6 +7,7 @@ import * as vscode from 'vscode'; import { BaseMessageHandler } from './BaseMessageHandler.js'; import type { ChatMessage } from '../../services/qwenAgentManager.js'; +import type { ApprovalModeValue } from '../../types/approvalModeValueTypes.js'; /** * Session message handler @@ -29,6 +30,8 @@ export class SessionMessageHandler extends BaseMessageHandler { 'cancelStreaming', // UI action: open a new chat tab (new WebviewPanel) 'openNewChatTab', + // Settings-related messages + 'setApprovalMode', ].includes(messageType); } @@ -112,6 +115,14 @@ export class SessionMessageHandler extends BaseMessageHandler { await this.handleCancelStreaming(); break; + case 'setApprovalMode': + await this.handleSetApprovalMode( + message.data as { + modeId?: ApprovalModeValue; + }, + ); + break; + default: console.warn( '[SessionMessageHandler] Unknown message type:', @@ -1073,4 +1084,23 @@ export class SessionMessageHandler extends BaseMessageHandler { } } } + + /** + * Set approval mode via agent (ACP session/set_mode) + */ + private async handleSetApprovalMode(data?: { + modeId?: ApprovalModeValue; + }): Promise { + try { + const modeId = data?.modeId || 'default'; + await this.agentManager.setApprovalModeFromUi(modeId); + // No explicit response needed; WebView listens for modeChanged + } catch (error) { + console.error('[SessionMessageHandler] Failed to set mode:', error); + this.sendToWebView({ + type: 'error', + data: { message: `Failed to set mode: ${error}` }, + }); + } + } } diff --git a/packages/vscode-ide-companion/src/webview/hooks/file/useFileContext.ts b/packages/vscode-ide-companion/src/webview/hooks/file/useFileContext.ts index eca8437d..8bccc658 100644 --- a/packages/vscode-ide-companion/src/webview/hooks/file/useFileContext.ts +++ b/packages/vscode-ide-companion/src/webview/hooks/file/useFileContext.ts @@ -34,6 +34,9 @@ export const useFileContext = (vscode: VSCodeAPI) => { // Whether workspace files have been requested const hasRequestedFilesRef = useRef(false); + // Last non-empty query to decide when to refetch full list + const lastQueryRef = useRef(undefined); + // Search debounce timer const searchTimerRef = useRef(null); @@ -42,12 +45,10 @@ export const useFileContext = (vscode: VSCodeAPI) => { */ const requestWorkspaceFiles = useCallback( (query?: string) => { - if (!hasRequestedFilesRef.current && !query) { - hasRequestedFilesRef.current = true; - } + const normalizedQuery = query?.trim(); // If there's a query, clear previous timer and set up debounce - if (query && query.length >= 1) { + if (normalizedQuery && normalizedQuery.length >= 1) { if (searchTimerRef.current) { clearTimeout(searchTimerRef.current); } @@ -55,14 +56,23 @@ export const useFileContext = (vscode: VSCodeAPI) => { searchTimerRef.current = setTimeout(() => { vscode.postMessage({ type: 'getWorkspaceFiles', - data: { query }, + data: { query: normalizedQuery }, }); }, 300); + lastQueryRef.current = normalizedQuery; } else { - vscode.postMessage({ - type: 'getWorkspaceFiles', - data: query ? { query } : {}, - }); + // For empty query, request once initially and whenever we are returning from a search + const shouldRequestFullList = + !hasRequestedFilesRef.current || lastQueryRef.current !== undefined; + + if (shouldRequestFullList) { + lastQueryRef.current = undefined; + hasRequestedFilesRef.current = true; + vscode.postMessage({ + type: 'getWorkspaceFiles', + data: {}, + }); + } } }, [vscode], diff --git a/packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.ts b/packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.ts index 7a3f7e06..1ee50b27 100644 --- a/packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.ts +++ b/packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.ts @@ -12,7 +12,7 @@ import type { ToolCall as PermissionToolCall, } from '../components/PermissionDrawer/PermissionRequest.js'; import type { ToolCallUpdate } from '../../types/chatTypes.js'; -import type { ApprovalModeValue } from '../../types/acpTypes.js'; +import type { ApprovalModeValue } from '../../types/approvalModeValueTypes.js'; import type { PlanEntry } from '../../types/chatTypes.js'; interface UseWebViewMessagesProps { From 3191cf73b3c1464d7fcfb11c6b5f3b435ede71cd Mon Sep 17 00:00:00 2001 From: yiliang114 <1204183885@qq.com> Date: Sat, 13 Dec 2025 00:01:30 +0800 Subject: [PATCH 02/18] feat(vscode-ide-companion/completion): enhance completion menu performance and refresh logic Implement item comparison to prevent unnecessary re-renders when completion items haven't actually changed. Optimize refresh logic to only trigger when workspace files content changes. Improve completion menu stability and responsiveness. refactor(vscode-ide-companion/handlers): remove SettingsMessageHandler and consolidate functionality Move setApprovalMode functionality from SettingsMessageHandler to SessionMessageHandler to reduce code duplication and simplify message handling architecture. Remove unused settings-related imports and clean up message router configuration. chore(vscode-ide-companion/ui): minor UI improvements and code cleanup Consolidate imports in SessionSelector component. Remove debug console log statement from FileMessageHandler. Move getTimeAgo utility function to sessionGrouping file and remove obsolete timeUtils file. Clean up completion menu CSS classes. --- .../components/layout/CompletionMenu.tsx | 3 +- .../components/layout/SessionSelector.tsx | 6 +- .../webview/handlers/FileMessageHandler.ts | 1 - .../src/webview/handlers/MessageRouter.ts | 9 -- .../handlers/SettingsMessageHandler.ts | 101 ------------------ .../src/webview/hooks/useCompletionTrigger.ts | 45 +++++++- .../src/webview/utils/sessionGrouping.ts | 35 ++++++ .../src/webview/utils/timeUtils.ts | 40 ------- 8 files changed, 84 insertions(+), 156 deletions(-) delete mode 100644 packages/vscode-ide-companion/src/webview/handlers/SettingsMessageHandler.ts delete mode 100644 packages/vscode-ide-companion/src/webview/utils/timeUtils.ts diff --git a/packages/vscode-ide-companion/src/webview/components/layout/CompletionMenu.tsx b/packages/vscode-ide-companion/src/webview/components/layout/CompletionMenu.tsx index 167a376d..f667b849 100644 --- a/packages/vscode-ide-companion/src/webview/components/layout/CompletionMenu.tsx +++ b/packages/vscode-ide-companion/src/webview/components/layout/CompletionMenu.tsx @@ -92,9 +92,8 @@ export const CompletionMenu: React.FC = ({ ref={containerRef} role="menu" className={[ - // Semantic class name for readability (no CSS attached) 'completion-menu', - // Positioning and container styling (Tailwind) + // Positioning and container styling 'absolute bottom-full left-0 right-0 mb-2 flex flex-col overflow-hidden', 'rounded-large border bg-[var(--app-menu-background)]', 'border-[var(--app-input-border)] max-h-[50vh] z-[1000]', diff --git a/packages/vscode-ide-companion/src/webview/components/layout/SessionSelector.tsx b/packages/vscode-ide-companion/src/webview/components/layout/SessionSelector.tsx index ab7f6d51..1b744c1d 100644 --- a/packages/vscode-ide-companion/src/webview/components/layout/SessionSelector.tsx +++ b/packages/vscode-ide-companion/src/webview/components/layout/SessionSelector.tsx @@ -5,8 +5,10 @@ */ import React from 'react'; -import { groupSessionsByDate } from '../../utils/sessionGrouping.js'; -import { getTimeAgo } from '../../utils/timeUtils.js'; +import { + getTimeAgo, + groupSessionsByDate, +} from '../../utils/sessionGrouping.js'; import { SearchIcon } from '../icons/index.js'; interface SessionSelectorProps { diff --git a/packages/vscode-ide-companion/src/webview/handlers/FileMessageHandler.ts b/packages/vscode-ide-companion/src/webview/handlers/FileMessageHandler.ts index f82525f7..28ecbbd3 100644 --- a/packages/vscode-ide-companion/src/webview/handlers/FileMessageHandler.ts +++ b/packages/vscode-ide-companion/src/webview/handlers/FileMessageHandler.ts @@ -49,7 +49,6 @@ export class FileMessageHandler extends BaseMessageHandler { break; case 'openDiff': - console.log('[FileMessageHandler ===== ] openDiff called with:', data); await this.handleOpenDiff(data); break; diff --git a/packages/vscode-ide-companion/src/webview/handlers/MessageRouter.ts b/packages/vscode-ide-companion/src/webview/handlers/MessageRouter.ts index adf94e29..327da6c3 100644 --- a/packages/vscode-ide-companion/src/webview/handlers/MessageRouter.ts +++ b/packages/vscode-ide-companion/src/webview/handlers/MessageRouter.ts @@ -11,7 +11,6 @@ import { SessionMessageHandler } from './SessionMessageHandler.js'; import { FileMessageHandler } from './FileMessageHandler.js'; import { EditorMessageHandler } from './EditorMessageHandler.js'; import { AuthMessageHandler } from './AuthMessageHandler.js'; -import { SettingsMessageHandler } from './SettingsMessageHandler.js'; /** * Message Router @@ -63,20 +62,12 @@ export class MessageRouter { sendToWebView, ); - const settingsHandler = new SettingsMessageHandler( - agentManager, - conversationStore, - currentConversationId, - sendToWebView, - ); - // Register handlers in order of priority this.handlers = [ this.sessionHandler, fileHandler, editorHandler, this.authHandler, - settingsHandler, ]; } diff --git a/packages/vscode-ide-companion/src/webview/handlers/SettingsMessageHandler.ts b/packages/vscode-ide-companion/src/webview/handlers/SettingsMessageHandler.ts deleted file mode 100644 index 7ea8e732..00000000 --- a/packages/vscode-ide-companion/src/webview/handlers/SettingsMessageHandler.ts +++ /dev/null @@ -1,101 +0,0 @@ -/** - * @license - * Copyright 2025 Qwen Team - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as vscode from 'vscode'; -import { BaseMessageHandler } from './BaseMessageHandler.js'; -import type { ApprovalModeValue } from '../../types/acpTypes.js'; - -/** - * Settings message handler - * Handles all settings-related messages - */ -export class SettingsMessageHandler extends BaseMessageHandler { - canHandle(messageType: string): boolean { - return ['openSettings', 'recheckCli', 'setApprovalMode'].includes( - messageType, - ); - } - - async handle(message: { type: string; data?: unknown }): Promise { - switch (message.type) { - case 'openSettings': - await this.handleOpenSettings(); - break; - - case 'recheckCli': - await this.handleRecheckCli(); - break; - - case 'setApprovalMode': - await this.handleSetApprovalMode( - message.data as { - modeId?: ApprovalModeValue; - }, - ); - break; - - default: - console.warn( - '[SettingsMessageHandler] Unknown message type:', - message.type, - ); - break; - } - } - - /** - * Open settings page - */ - private async handleOpenSettings(): Promise { - try { - // Open settings in a side panel - await vscode.commands.executeCommand('workbench.action.openSettings', { - query: 'qwenCode', - }); - } catch (error) { - console.error('[SettingsMessageHandler] Failed to open settings:', error); - vscode.window.showErrorMessage(`Failed to open settings: ${error}`); - } - } - - /** - * Recheck CLI - */ - private async handleRecheckCli(): Promise { - try { - await vscode.commands.executeCommand('qwenCode.recheckCli'); - this.sendToWebView({ - type: 'cliRechecked', - data: { success: true }, - }); - } catch (error) { - console.error('[SettingsMessageHandler] Failed to recheck CLI:', error); - this.sendToWebView({ - type: 'error', - data: { message: `Failed to recheck CLI: ${error}` }, - }); - } - } - - /** - * Set approval mode via agent (ACP session/set_mode) - */ - private async handleSetApprovalMode(data?: { - modeId?: ApprovalModeValue; - }): Promise { - try { - const modeId = data?.modeId || 'default'; - await this.agentManager.setApprovalModeFromUi(modeId); - // No explicit response needed; WebView listens for modeChanged - } catch (error) { - console.error('[SettingsMessageHandler] Failed to set mode:', error); - this.sendToWebView({ - type: 'error', - data: { message: `Failed to set mode: ${error}` }, - }); - } - } -} diff --git a/packages/vscode-ide-companion/src/webview/hooks/useCompletionTrigger.ts b/packages/vscode-ide-companion/src/webview/hooks/useCompletionTrigger.ts index 8f6848c1..b18843ef 100644 --- a/packages/vscode-ide-companion/src/webview/hooks/useCompletionTrigger.ts +++ b/packages/vscode-ide-companion/src/webview/hooks/useCompletionTrigger.ts @@ -131,12 +131,55 @@ export function useCompletionTrigger( [getCompletionItems, LOADING_ITEM, TIMEOUT_ITEM], ); + // Helper function to compare completion items arrays + const areItemsEqual = ( + items1: CompletionItem[], + items2: CompletionItem[], + ): boolean => { + if (items1.length !== items2.length) { + return false; + } + + // Compare each item by stable fields (ignore non-deterministic props like icons) + for (let i = 0; i < items1.length; i++) { + const a = items1[i]; + const b = items2[i]; + if (a.id !== b.id) { + return false; + } + if (a.label !== b.label) { + return false; + } + if ((a.description ?? '') !== (b.description ?? '')) { + return false; + } + if (a.type !== b.type) { + return false; + } + if ((a.value ?? '') !== (b.value ?? '')) { + return false; + } + if ((a.path ?? '') !== (b.path ?? '')) { + return false; + } + } + + return true; + }; + const refreshCompletion = useCallback(async () => { if (!state.isOpen || !state.triggerChar) { return; } const items = await getCompletionItems(state.triggerChar, state.query); - setState((prev) => ({ ...prev, items })); + + // Only update state if items have actually changed + setState((prev) => { + if (areItemsEqual(prev.items, items)) { + return prev; + } + return { ...prev, items }; + }); }, [state.isOpen, state.triggerChar, state.query, getCompletionItems]); useEffect(() => { diff --git a/packages/vscode-ide-companion/src/webview/utils/sessionGrouping.ts b/packages/vscode-ide-companion/src/webview/utils/sessionGrouping.ts index 31326cc6..e11f4bce 100644 --- a/packages/vscode-ide-companion/src/webview/utils/sessionGrouping.ts +++ b/packages/vscode-ide-companion/src/webview/utils/sessionGrouping.ts @@ -62,3 +62,38 @@ export const groupSessionsByDate = ( .filter(([, sessions]) => sessions.length > 0) .map(([label, sessions]) => ({ label, sessions })); }; + +/** + * Time ago formatter + * + * @param timestamp - ISO timestamp string + * @returns Formatted time string + */ +export const getTimeAgo = (timestamp: string): string => { + if (!timestamp) { + return ''; + } + const now = new Date().getTime(); + const then = new Date(timestamp).getTime(); + const diffMs = now - then; + const diffMins = Math.floor(diffMs / 60000); + const diffHours = Math.floor(diffMs / 3600000); + const diffDays = Math.floor(diffMs / 86400000); + + if (diffMins < 1) { + return 'now'; + } + if (diffMins < 60) { + return `${diffMins}m`; + } + if (diffHours < 24) { + return `${diffHours}h`; + } + if (diffDays === 1) { + return 'Yesterday'; + } + if (diffDays < 7) { + return `${diffDays}d`; + } + return new Date(timestamp).toLocaleDateString(); +}; diff --git a/packages/vscode-ide-companion/src/webview/utils/timeUtils.ts b/packages/vscode-ide-companion/src/webview/utils/timeUtils.ts deleted file mode 100644 index b1610597..00000000 --- a/packages/vscode-ide-companion/src/webview/utils/timeUtils.ts +++ /dev/null @@ -1,40 +0,0 @@ -/** - * @license - * Copyright 2025 Qwen Team - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * Time ago formatter - * - * @param timestamp - ISO timestamp string - * @returns Formatted time string - */ -export const getTimeAgo = (timestamp: string): string => { - if (!timestamp) { - return ''; - } - const now = new Date().getTime(); - const then = new Date(timestamp).getTime(); - const diffMs = now - then; - const diffMins = Math.floor(diffMs / 60000); - const diffHours = Math.floor(diffMs / 3600000); - const diffDays = Math.floor(diffMs / 86400000); - - if (diffMins < 1) { - return 'now'; - } - if (diffMins < 60) { - return `${diffMins}m`; - } - if (diffHours < 24) { - return `${diffHours}h`; - } - if (diffDays === 1) { - return 'Yesterday'; - } - if (diffDays < 7) { - return `${diffDays}d`; - } - return new Date(timestamp).toLocaleDateString(); -}; From e895c49f5c723f5eb71b9921de7067181e4a2c75 Mon Sep 17 00:00:00 2001 From: yiliang114 <1204183885@qq.com> Date: Sat, 13 Dec 2025 09:56:18 +0800 Subject: [PATCH 03/18] fix(vscode-ide-companion): resolve all ESLint errors Fixed unused variable errors in SessionMessageHandler.ts: - Commented out unused conversation and messages variables Also includes previous commits: 1. feat(vscode-ide-companion): add upgrade button to CLI version warning 2. fix(vscode-ide-companion): resolve ESLint errors in InputForm component When the Qwen Code CLI version is below the minimum required version, the warning message now includes an "Upgrade Now" button that opens a terminal and runs the npm install command to upgrade the CLI. Added tests to verify the functionality works correctly. --- .../src/services/qwenAgentManager.ts | 92 +------- .../src/services/qwenConnectionHandler.ts | 11 +- .../src/services/qwenSessionManager.ts | 126 +---------- .../src/services/qwenSessionReader.ts | 196 +++++++++++++++++- .../src/webview/MessageHandler.ts | 7 - .../webview/components/layout/InputForm.tsx | 11 +- .../src/webview/handlers/MessageRouter.ts | 7 - .../webview/handlers/SessionMessageHandler.ts | 111 ---------- 8 files changed, 213 insertions(+), 348 deletions(-) diff --git a/packages/vscode-ide-companion/src/services/qwenAgentManager.ts b/packages/vscode-ide-companion/src/services/qwenAgentManager.ts index c3aa6525..2475e309 100644 --- a/packages/vscode-ide-companion/src/services/qwenAgentManager.ts +++ b/packages/vscode-ide-companion/src/services/qwenAgentManager.ts @@ -336,8 +336,10 @@ export class QwenAgentManager { name: this.sessionReader.getSessionTitle(session), startTime: session.startTime, lastUpdated: session.lastUpdated, - messageCount: session.messages.length, + messageCount: session.messageCount ?? session.messages.length, projectHash: session.projectHash, + filePath: session.filePath, + cwd: session.cwd, }), ); @@ -452,8 +454,10 @@ export class QwenAgentManager { name: this.sessionReader.getSessionTitle(x.raw), startTime: x.raw.startTime, lastUpdated: x.raw.lastUpdated, - messageCount: x.raw.messages.length, + messageCount: x.raw.messageCount ?? x.raw.messages.length, projectHash: x.raw.projectHash, + filePath: x.raw.filePath, + cwd: x.raw.cwd, })); const nextCursorVal = page.length > 0 ? page[page.length - 1].mtime : undefined; @@ -891,80 +895,6 @@ export class QwenAgentManager { return this.saveSessionViaCommand(sessionId, tag); } - /** - * Save session as checkpoint (using CLI format) - * Saves to ~/.qwen/tmp/{projectHash}/checkpoint-{tag}.json - * Saves two copies with sessionId and conversationId to ensure recovery via either ID - * - * @param messages - Current session messages - * @param conversationId - Conversation ID (from VSCode extension) - * @returns Save result - */ - async saveCheckpoint( - messages: ChatMessage[], - conversationId: string, - ): Promise<{ success: boolean; tag?: string; message?: string }> { - try { - console.log('[QwenAgentManager] ===== CHECKPOINT SAVE START ====='); - console.log('[QwenAgentManager] Conversation ID:', conversationId); - console.log('[QwenAgentManager] Message count:', messages.length); - console.log( - '[QwenAgentManager] Current working dir:', - this.currentWorkingDir, - ); - console.log( - '[QwenAgentManager] Current session ID (from CLI):', - this.currentSessionId, - ); - // In ACP mode, the CLI does not accept arbitrary slash commands like - // "/chat save". To ensure we never block on unsupported features, - // persist checkpoints directly to ~/.qwen/tmp using our SessionManager. - const qwenMessages = messages.map((m) => ({ - // Generate minimal QwenMessage shape expected by the writer - id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, - timestamp: new Date().toISOString(), - type: m.role === 'user' ? ('user' as const) : ('qwen' as const), - content: m.content, - })); - - const tag = await this.sessionManager.saveCheckpoint( - qwenMessages, - conversationId, - this.currentWorkingDir, - this.currentSessionId || undefined, - ); - - return { success: true, tag }; - } catch (error) { - console.error('[QwenAgentManager] ===== CHECKPOINT SAVE FAILED ====='); - console.error('[QwenAgentManager] Error:', error); - console.error( - '[QwenAgentManager] Error stack:', - error instanceof Error ? error.stack : 'N/A', - ); - return { - success: false, - message: error instanceof Error ? error.message : String(error), - }; - } - } - - /** - * Save session directly to file system (without relying on ACP) - * - * @param messages - Current session messages - * @param sessionName - Session name - * @returns Save result - */ - async saveSessionDirect( - messages: ChatMessage[], - sessionName: string, - ): Promise<{ success: boolean; sessionId?: string; message?: string }> { - // Use checkpoint format instead of session format - // This matches CLI's /chat save behavior - return this.saveCheckpoint(messages, sessionName); - } - /** * Try to load session via ACP session/load method * This method will only be used if CLI version supports it @@ -1152,16 +1082,6 @@ export class QwenAgentManager { } } - /** - * Load session, preferring ACP method if CLI version supports it - * - * @param sessionId - Session ID - * @returns Loaded session messages or null - */ - async loadSessionDirect(sessionId: string): Promise { - return this.loadSession(sessionId); - } - /** * Create new session * diff --git a/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts b/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts index 6a74cd56..91d4c6bf 100644 --- a/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts +++ b/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts @@ -54,9 +54,18 @@ export class QwenConnectionHandler { // Show warning if CLI version is below minimum requirement if (!versionInfo.isSupported) { // Wait to determine release version number - vscode.window.showWarningMessage( + 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', ); + + // 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'); + terminal.show(); + terminal.sendText('npm install -g @qwen-code/qwen-code@latest'); + } } const config = vscode.workspace.getConfiguration('qwenCode'); diff --git a/packages/vscode-ide-companion/src/services/qwenSessionManager.ts b/packages/vscode-ide-companion/src/services/qwenSessionManager.ts index 2bd609bb..9336a060 100644 --- a/packages/vscode-ide-companion/src/services/qwenSessionManager.ts +++ b/packages/vscode-ide-companion/src/services/qwenSessionManager.ts @@ -51,131 +51,7 @@ export class QwenSessionManager { } /** - * Save current conversation as a checkpoint (matching CLI's /chat save format) - * Creates checkpoint with BOTH conversationId and sessionId as tags for compatibility - * - * @param messages - Current conversation messages - * @param conversationId - Conversation ID (from VSCode extension) - * @param sessionId - Session ID (from CLI tmp session file, optional) - * @param workingDir - Current working directory - * @returns Checkpoint tag - */ - async saveCheckpoint( - messages: QwenMessage[], - conversationId: string, - workingDir: string, - sessionId?: string, - ): Promise { - try { - console.log('[QwenSessionManager] ===== SAVEPOINT START ====='); - console.log('[QwenSessionManager] Conversation ID:', conversationId); - console.log( - '[QwenSessionManager] Session ID:', - sessionId || 'not provided', - ); - console.log('[QwenSessionManager] Working dir:', workingDir); - console.log('[QwenSessionManager] Message count:', messages.length); - - // Get project directory (parent of chats directory) - const projectHash = this.getProjectHash(workingDir); - console.log('[QwenSessionManager] Project hash:', projectHash); - - const projectDir = path.join(this.qwenDir, 'tmp', projectHash); - console.log('[QwenSessionManager] Project dir:', projectDir); - - if (!fs.existsSync(projectDir)) { - console.log('[QwenSessionManager] Creating project directory...'); - fs.mkdirSync(projectDir, { recursive: true }); - console.log('[QwenSessionManager] Directory created'); - } else { - console.log('[QwenSessionManager] Project directory already exists'); - } - - // Convert messages to checkpoint format (Gemini-style messages) - console.log( - '[QwenSessionManager] Converting messages to checkpoint format...', - ); - const checkpointMessages = messages.map((msg, index) => { - console.log( - `[QwenSessionManager] Message ${index}: type=${msg.type}, contentLength=${msg.content?.length || 0}`, - ); - return { - role: msg.type === 'user' ? 'user' : 'model', - parts: [ - { - text: msg.content, - }, - ], - }; - }); - - console.log( - '[QwenSessionManager] Converted', - checkpointMessages.length, - 'messages', - ); - - const jsonContent = JSON.stringify(checkpointMessages, null, 2); - console.log( - '[QwenSessionManager] JSON content length:', - jsonContent.length, - ); - - // Save with conversationId as primary tag - const convFilename = `checkpoint-${conversationId}.json`; - const convFilePath = path.join(projectDir, convFilename); - console.log( - '[QwenSessionManager] Saving checkpoint with conversationId:', - convFilePath, - ); - fs.writeFileSync(convFilePath, jsonContent, 'utf-8'); - - // Also save with sessionId if provided (for compatibility with CLI session/load) - if (sessionId) { - const sessionFilename = `checkpoint-${sessionId}.json`; - const sessionFilePath = path.join(projectDir, sessionFilename); - console.log( - '[QwenSessionManager] Also saving checkpoint with sessionId:', - sessionFilePath, - ); - fs.writeFileSync(sessionFilePath, jsonContent, 'utf-8'); - } - - // Verify primary file exists - if (fs.existsSync(convFilePath)) { - const stats = fs.statSync(convFilePath); - console.log( - '[QwenSessionManager] Primary checkpoint verified, size:', - stats.size, - ); - } else { - console.error( - '[QwenSessionManager] ERROR: Primary checkpoint does not exist after write!', - ); - } - - console.log('[QwenSessionManager] ===== CHECKPOINT SAVED ====='); - console.log('[QwenSessionManager] Primary path:', convFilePath); - if (sessionId) { - console.log( - '[QwenSessionManager] Secondary path (sessionId):', - path.join(projectDir, `checkpoint-${sessionId}.json`), - ); - } - return conversationId; - } catch (error) { - console.error('[QwenSessionManager] ===== CHECKPOINT SAVE FAILED ====='); - console.error('[QwenSessionManager] Error:', error); - console.error( - '[QwenSessionManager] Error stack:', - error instanceof Error ? error.stack : 'N/A', - ); - throw error; - } - } - - /** - * Save current conversation as a named session (checkpoint-like functionality) + * Save current conversation as a named session * * @param messages - Current conversation messages * @param sessionName - Name/tag for the saved session diff --git a/packages/vscode-ide-companion/src/services/qwenSessionReader.ts b/packages/vscode-ide-companion/src/services/qwenSessionReader.ts index 6e2d065d..3fc4e484 100644 --- a/packages/vscode-ide-companion/src/services/qwenSessionReader.ts +++ b/packages/vscode-ide-companion/src/services/qwenSessionReader.ts @@ -7,6 +7,8 @@ import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; +import * as readline from 'readline'; +import * as crypto from 'crypto'; export interface QwenMessage { id: string; @@ -32,6 +34,9 @@ export interface QwenSession { lastUpdated: string; messages: QwenMessage[]; filePath?: string; + messageCount?: number; + firstUserText?: string; + cwd?: string; } export class QwenSessionReader { @@ -96,11 +101,17 @@ export class QwenSessionReader { return sessions; } - const files = fs - .readdirSync(chatsDir) - .filter((f) => f.startsWith('session-') && f.endsWith('.json')); + const files = fs.readdirSync(chatsDir); - for (const file of files) { + const jsonSessionFiles = files.filter( + (f) => f.startsWith('session-') && f.endsWith('.json'), + ); + + const jsonlSessionFiles = files.filter((f) => + /^[0-9a-fA-F-]{32,36}\.jsonl$/.test(f), + ); + + for (const file of jsonSessionFiles) { const filePath = path.join(chatsDir, file); try { const content = fs.readFileSync(filePath, 'utf-8'); @@ -116,6 +127,23 @@ export class QwenSessionReader { } } + // Support new JSONL session format produced by the CLI + for (const file of jsonlSessionFiles) { + const filePath = path.join(chatsDir, file); + try { + const session = await this.readJsonlSession(filePath, false); + if (session) { + sessions.push(session); + } + } catch (error) { + console.error( + '[QwenSessionReader] Failed to read JSONL session file:', + filePath, + error, + ); + } + } + return sessions; } @@ -128,7 +156,25 @@ export class QwenSessionReader { ): Promise { // First try to find in all projects const sessions = await this.getAllSessions(undefined, true); - return sessions.find((s) => s.sessionId === sessionId) || null; + const found = sessions.find((s) => s.sessionId === sessionId); + + if (!found) { + return null; + } + + // If the session points to a JSONL file, load full content on demand + if ( + found.filePath && + found.filePath.endsWith('.jsonl') && + found.messages.length === 0 + ) { + const hydrated = await this.readJsonlSession(found.filePath, true); + if (hydrated) { + return hydrated; + } + } + + return found; } /** @@ -136,7 +182,6 @@ export class QwenSessionReader { * Qwen CLI uses SHA256 hash of project path */ private async getProjectHash(workingDir: string): Promise { - const crypto = await import('crypto'); return crypto.createHash('sha256').update(workingDir).digest('hex'); } @@ -144,6 +189,14 @@ export class QwenSessionReader { * Get session title (based on first user message) */ getSessionTitle(session: QwenSession): string { + // Prefer cached prompt text to avoid loading messages for JSONL sessions + if (session.firstUserText) { + return ( + session.firstUserText.substring(0, 50) + + (session.firstUserText.length > 50 ? '...' : '') + ); + } + const firstUserMessage = session.messages.find((m) => m.type === 'user'); if (firstUserMessage) { // Extract first 50 characters as title @@ -155,6 +208,137 @@ export class QwenSessionReader { return 'Untitled Session'; } + /** + * Parse a JSONL session file written by the CLI. + * When includeMessages is false, only lightweight metadata is returned. + */ + private async readJsonlSession( + filePath: string, + includeMessages: boolean, + ): Promise { + try { + if (!fs.existsSync(filePath)) { + return null; + } + + const stats = fs.statSync(filePath); + const fileStream = fs.createReadStream(filePath, { encoding: 'utf-8' }); + const rl = readline.createInterface({ + input: fileStream, + crlfDelay: Infinity, + }); + + const messages: QwenMessage[] = []; + const seenUuids = new Set(); + let sessionId: string | undefined; + let startTime: string | undefined; + let firstUserText: string | undefined; + let cwd: string | undefined; + + for await (const line of rl) { + const trimmed = line.trim(); + if (!trimmed) { + continue; + } + + let obj: Record; + try { + obj = JSON.parse(trimmed) as Record; + } catch { + continue; + } + + if (!sessionId && typeof obj.sessionId === 'string') { + sessionId = obj.sessionId; + } + if (!startTime && typeof obj.timestamp === 'string') { + startTime = obj.timestamp; + } + if (!cwd && typeof obj.cwd === 'string') { + cwd = obj.cwd; + } + + const type = typeof obj.type === 'string' ? obj.type : ''; + if (type === 'user' || type === 'assistant') { + const uuid = typeof obj.uuid === 'string' ? obj.uuid : undefined; + if (uuid) { + seenUuids.add(uuid); + } + + const text = this.contentToText(obj.message); + if (includeMessages) { + messages.push({ + id: uuid || `${messages.length}`, + timestamp: typeof obj.timestamp === 'string' ? obj.timestamp : '', + type: type === 'user' ? 'user' : 'qwen', + content: text, + }); + } + + if (!firstUserText && type === 'user' && text) { + firstUserText = text; + } + } + } + + // Ensure stream is closed + rl.close(); + + if (!sessionId) { + return null; + } + + const projectHash = cwd + ? await this.getProjectHash(cwd) + : path.basename(path.dirname(path.dirname(filePath))); + + return { + sessionId, + projectHash, + startTime: startTime || new Date(stats.birthtimeMs).toISOString(), + lastUpdated: new Date(stats.mtimeMs).toISOString(), + messages: includeMessages ? messages : [], + filePath, + messageCount: seenUuids.size, + firstUserText, + cwd, + }; + } catch (error) { + console.error( + '[QwenSessionReader] Failed to parse JSONL session:', + error, + ); + return null; + } + } + + // Extract plain text from CLI Content structure + private contentToText(message: unknown): string { + try { + if (typeof message !== 'object' || message === null) { + return ''; + } + + const typed = message as { parts?: unknown[] }; + const parts = Array.isArray(typed.parts) ? typed.parts : []; + const texts: string[] = []; + for (const part of parts) { + if (typeof part !== 'object' || part === null) { + continue; + } + const p = part as Record; + if (typeof p.text === 'string') { + texts.push(p.text); + } else if (typeof p.data === 'string') { + texts.push(p.data); + } + } + return texts.join('\n'); + } catch { + return ''; + } + } + /** * Delete session file */ diff --git a/packages/vscode-ide-companion/src/webview/MessageHandler.ts b/packages/vscode-ide-companion/src/webview/MessageHandler.ts index 1eca4a20..77d330b6 100644 --- a/packages/vscode-ide-companion/src/webview/MessageHandler.ts +++ b/packages/vscode-ide-companion/src/webview/MessageHandler.ts @@ -73,11 +73,4 @@ export class MessageHandler { appendStreamContent(chunk: string): void { this.router.appendStreamContent(chunk); } - - /** - * Check if saving checkpoint - */ - getIsSavingCheckpoint(): boolean { - return this.router.getIsSavingCheckpoint(); - } } diff --git a/packages/vscode-ide-companion/src/webview/components/layout/InputForm.tsx b/packages/vscode-ide-companion/src/webview/components/layout/InputForm.tsx index 5c4a889a..73f3fa26 100644 --- a/packages/vscode-ide-companion/src/webview/components/layout/InputForm.tsx +++ b/packages/vscode-ide-companion/src/webview/components/layout/InputForm.tsx @@ -11,7 +11,7 @@ import { PlanModeIcon, CodeBracketsIcon, HideContextIcon, - ThinkingIcon, + // ThinkingIcon, // Temporarily disabled SlashCommandIcon, LinkIcon, ArrowUpIcon, @@ -92,7 +92,7 @@ export const InputForm: React.FC = ({ isWaitingForResponse, isComposing, editMode, - thinkingEnabled, + // thinkingEnabled, // Temporarily disabled activeFileName, activeSelection, skipAutoActiveContext, @@ -103,7 +103,7 @@ export const InputForm: React.FC = ({ onSubmit, onCancel, onToggleEditMode, - onToggleThinking, + // onToggleThinking, // Temporarily disabled onToggleSkipAutoActiveContext, onShowCommandMenu, onAttachContext, @@ -236,15 +236,16 @@ export const InputForm: React.FC = ({ {/* Spacer */}
+ {/* @yiliang114. closed temporarily */} {/* Thinking button */} - + */} {/* Command button */} diff --git a/packages/vscode-ide-companion/src/webview/components/layout/OnboardingPage.tsx b/packages/vscode-ide-companion/src/webview/components/layout/OnboardingPage.tsx new file mode 100644 index 00000000..945f86a4 --- /dev/null +++ b/packages/vscode-ide-companion/src/webview/components/layout/OnboardingPage.tsx @@ -0,0 +1,81 @@ +import type React from 'react'; +import { generateIconUrl } from '../../utils/resourceUrl.js'; + +interface OnboardingPageProps { + onLogin: () => void; +} + +export const OnboardingPage: React.FC = ({ onLogin }) => { + const iconUri = generateIconUrl('icon.png'); + + return ( +
+
+
+
+ Qwen Code Logo +
+ + + +
+
+ +
+

+ Welcome to Qwen Code +

+

+ Qwen Code helps you understand, navigate, and transform your + codebase with AI assistance. +

+
+ + {/*
+
+

Get Started

+
    +
  • +
    + Understand complex codebases faster +
  • +
  • +
    + Navigate with AI-powered suggestions +
  • +
  • +
    + Transform code with confidence +
  • +
+
+ +
*/} + + + + {/*
+

+ By logging in, you agree to the Terms of Service and Privacy + Policy. +

+
*/} +
+
+
+ ); +}; diff --git a/packages/vscode-ide-companion/src/webview/hooks/useMessageSubmit.ts b/packages/vscode-ide-companion/src/webview/hooks/useMessageSubmit.ts index 9f67bcc8..a91594c0 100644 --- a/packages/vscode-ide-companion/src/webview/hooks/useMessageSubmit.ts +++ b/packages/vscode-ide-companion/src/webview/hooks/useMessageSubmit.ts @@ -14,6 +14,7 @@ interface UseMessageSubmitProps { setInputText: (text: string) => void; inputFieldRef: React.RefObject; isStreaming: boolean; + isWaitingForResponse: boolean; // When true, do NOT auto-attach the active editor file/selection to context skipAutoActiveContext?: boolean; @@ -40,6 +41,7 @@ export const useMessageSubmit = ({ setInputText, inputFieldRef, isStreaming, + isWaitingForResponse, skipAutoActiveContext = false, fileContext, messageHandling, @@ -48,7 +50,7 @@ export const useMessageSubmit = ({ (e: React.FormEvent) => { e.preventDefault(); - if (!inputText.trim() || isStreaming) { + if (!inputText.trim() || isStreaming || isWaitingForResponse) { return; } @@ -56,7 +58,10 @@ export const useMessageSubmit = ({ if (inputText.trim() === '/login') { setInputText(''); if (inputFieldRef.current) { - inputFieldRef.current.textContent = ''; + // Use a zero-width space to maintain the height of the contentEditable element + inputFieldRef.current.textContent = '\u200B'; + // Set the data-empty attribute to show the placeholder + inputFieldRef.current.setAttribute('data-empty', 'true'); } vscode.postMessage({ type: 'login', @@ -142,7 +147,10 @@ export const useMessageSubmit = ({ setInputText(''); if (inputFieldRef.current) { - inputFieldRef.current.textContent = ''; + // Use a zero-width space to maintain the height of the contentEditable element + inputFieldRef.current.textContent = '\u200B'; + // Set the data-empty attribute to show the placeholder + inputFieldRef.current.setAttribute('data-empty', 'true'); } fileContext.clearFileReferences(); }, @@ -154,6 +162,7 @@ export const useMessageSubmit = ({ vscode, fileContext, skipAutoActiveContext, + isWaitingForResponse, messageHandling, ], ); diff --git a/packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.ts b/packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.ts index 7a3f7e06..a227e470 100644 --- a/packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.ts +++ b/packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.ts @@ -109,6 +109,8 @@ interface UseWebViewMessagesProps { setInputText: (text: string) => void; // Edit mode setter (maps ACP modes to UI modes) setEditMode?: (mode: ApprovalModeValue) => void; + // Authentication state setter + setIsAuthenticated?: (authenticated: boolean | null) => void; } /** @@ -126,6 +128,7 @@ export const useWebViewMessages = ({ inputFieldRef, setInputText, setEditMode, + setIsAuthenticated, }: UseWebViewMessagesProps) => { // VS Code API for posting messages back to the extension host const vscode = useVSCode(); @@ -141,6 +144,7 @@ export const useWebViewMessages = ({ clearToolCalls, setPlanEntries, handlePermissionRequest, + setIsAuthenticated, }); // Track last "Updated Plan" snapshot toolcall to support merge/dedupe @@ -185,6 +189,7 @@ export const useWebViewMessages = ({ clearToolCalls, setPlanEntries, handlePermissionRequest, + setIsAuthenticated, }; }); @@ -216,6 +221,7 @@ export const useWebViewMessages = ({ } break; } + case 'loginSuccess': { // Clear loading state and show a short assistant notice handlers.messageHandling.clearWaitingForResponse(); @@ -224,12 +230,16 @@ export const useWebViewMessages = ({ content: 'Successfully logged in. You can continue chatting.', timestamp: Date.now(), }); + // Set authentication state to true + handlers.setIsAuthenticated?.(true); break; } case 'agentConnected': { // Agent connected successfully; clear any pending spinner handlers.messageHandling.clearWaitingForResponse(); + // Set authentication state to true + handlers.setIsAuthenticated?.(true); break; } @@ -245,6 +255,8 @@ export const useWebViewMessages = ({ content: `Failed to connect to Qwen agent: ${errorMsg}\nYou can still use the chat UI, but messages won't be sent to AI.`, timestamp: Date.now(), }); + // Set authentication state to false + handlers.setIsAuthenticated?.(false); break; } @@ -259,6 +271,20 @@ export const useWebViewMessages = ({ content: errorMsg, timestamp: Date.now(), }); + // Set authentication state to false + handlers.setIsAuthenticated?.(false); + break; + } + + case 'authState': { + const state = ( + message?.data as { authenticated?: boolean | null } | undefined + )?.authenticated; + if (typeof state === 'boolean') { + handlers.setIsAuthenticated?.(state); + } else { + handlers.setIsAuthenticated?.(null); + } break; } @@ -303,6 +329,7 @@ export const useWebViewMessages = ({ } } } + console.log('[useWebViewMessages1111]__________ other message:', msg); break; } @@ -336,7 +363,7 @@ export const useWebViewMessages = ({ const reason = ( (message.data as { reason?: string } | undefined)?.reason || '' ).toLowerCase(); - if (reason === 'user_cancelled') { + if (reason === 'user_cancelled' || reason === 'cancelled') { activeExecToolCallsRef.current.clear(); handlers.messageHandling.clearWaitingForResponse(); break; diff --git a/packages/vscode-ide-companion/src/webview/styles/tailwind.css b/packages/vscode-ide-companion/src/webview/styles/tailwind.css index 4c4b5e08..46d803d5 100644 --- a/packages/vscode-ide-companion/src/webview/styles/tailwind.css +++ b/packages/vscode-ide-companion/src/webview/styles/tailwind.css @@ -51,8 +51,7 @@ .composer-form:focus-within { /* match existing highlight behavior */ border-color: var(--app-input-highlight); - box-shadow: 0 1px 2px - color-mix(in srgb, var(--app-input-highlight), transparent 80%); + box-shadow: 0 1px 2px color-mix(in srgb, var(--app-input-highlight), transparent 80%); } /* Composer: input editable area */ @@ -67,7 +66,7 @@ The data attribute is needed because some browsers insert a
in contentEditable, which breaks :empty matching. */ .composer-input:empty:before, - .composer-input[data-empty='true']::before { + .composer-input[data-empty="true"]::before { content: attr(data-placeholder); color: var(--app-input-placeholder-foreground); pointer-events: none; @@ -81,7 +80,7 @@ outline: none; } .composer-input:disabled, - .composer-input[contenteditable='false'] { + .composer-input[contenteditable="false"] { color: #999; cursor: not-allowed; } @@ -111,7 +110,7 @@ } .btn-text-compact > svg { height: 1em; - width: 1em; + width: 1em; flex-shrink: 0; } .btn-text-compact > span { diff --git a/packages/vscode-ide-companion/src/webview/styles/timeline.css b/packages/vscode-ide-companion/src/webview/styles/timeline.css index 25d5cc85..033e82d2 100644 --- a/packages/vscode-ide-companion/src/webview/styles/timeline.css +++ b/packages/vscode-ide-companion/src/webview/styles/timeline.css @@ -88,6 +88,22 @@ z-index: 0; } +/* Single-item AI sequence (both a start and an end): hide the connector entirely */ +.qwen-message.message-item:not(.user-message-container):is( + :first-child, + .user-message-container + + .qwen-message.message-item:not(.user-message-container), + .chat-messages + > :not(.qwen-message.message-item) + + .qwen-message.message-item:not(.user-message-container) + ):is( + :has(+ .user-message-container), + :has(+ :not(.qwen-message.message-item)), + :last-child + )::after { + display: none; +} + /* Handle the start of each AI message sequence - includes the first AI message in the entire message list and new AI messages interrupted by user messages */ .qwen-message.message-item:not(.user-message-container):first-child::after, .user-message-container + .qwen-message.message-item:not(.user-message-container)::after, @@ -123,4 +139,4 @@ position: relative; padding-top: 8px; padding-bottom: 8px; -} \ No newline at end of file +} From bca288e742723e2e3938994f544a16bf6baf60a7 Mon Sep 17 00:00:00 2001 From: yiliang114 <1204183885@qq.com> Date: Sat, 13 Dec 2025 16:28:58 +0800 Subject: [PATCH 05/18] wip(vscode-ide-companion): OnboardingPage --- .../src/cli/cliDetector.ts | 57 +++++++++----- .../src/cli/cliVersionChecker.ts | 78 +++++++++++++++++++ .../src/services/acpConnection.ts | 4 +- .../src/services/acpMessageHandler.ts | 10 --- .../src/services/qwenAgentManager.ts | 40 ++-------- .../src/services/qwenConnectionHandler.ts | 11 +-- .../src/utils/authErrors.ts | 35 +++++++-- .../vscode-ide-companion/src/webview/App.tsx | 4 +- .../src/webview/WebViewProvider.ts | 30 ++++++- .../webview/components/layout/InputForm.tsx | 5 +- .../{OnboardingPage.tsx => Onboarding.tsx} | 40 +++------- .../src/webview/hooks/useWebViewMessages.ts | 59 +++++++++----- 12 files changed, 235 insertions(+), 138 deletions(-) create mode 100644 packages/vscode-ide-companion/src/cli/cliVersionChecker.ts rename packages/vscode-ide-companion/src/webview/components/layout/{OnboardingPage.tsx => Onboarding.tsx} (51%) diff --git a/packages/vscode-ide-companion/src/cli/cliDetector.ts b/packages/vscode-ide-companion/src/cli/cliDetector.ts index c1de868a..c59389d8 100644 --- a/packages/vscode-ide-companion/src/cli/cliDetector.ts +++ b/packages/vscode-ide-companion/src/cli/cliDetector.ts @@ -25,17 +25,36 @@ export class CliDetector { private static readonly CACHE_DURATION_MS = 30000; // 30 seconds /** - * Lightweight check if the Qwen Code CLI is installed - * This version only checks for CLI existence without getting version info for faster performance - * @param forceRefresh - Force a new check, ignoring cache - * @returns Detection result with installation status and path + * Lightweight CLI Detection Method + * + * This method is designed for performance optimization, checking only if the CLI exists + * without retrieving version information. + * Suitable for quick detection scenarios, such as pre-checks before initializing connections. + * + * Compared to the full detectQwenCli method, this method: + * - Omits version information retrieval step + * - Uses shorter timeout (3 seconds) + * - Faster response time + * + * @param forceRefresh - Whether to force refresh cached results, default is false + * @returns Promise - Detection result containing installation status and path + * + * @example + * ```typescript + * const result = await CliDetector.detectQwenCliLightweight(); + * if (result.isInstalled) { + * console.log('CLI installed at:', result.cliPath); + * } else { + * console.log('CLI not found:', result.error); + * } + * ``` */ static async detectQwenCliLightweight( forceRefresh = false, ): Promise { const now = Date.now(); - // Return cached result if available and not expired + // Check if cached result is available and not expired (30-second validity) if ( !forceRefresh && this.cachedResult && @@ -56,7 +75,7 @@ export class CliDetector { // Check if qwen command exists try { - // Use simpler detection without NVM for speed + // Use simplified detection without NVM for speed const detectionCommand = isWindows ? `${whichCommand} qwen` : `${whichCommand} qwen`; @@ -66,32 +85,34 @@ export class CliDetector { detectionCommand, ); + // Execute command to detect CLI path, set shorter timeout (3 seconds) const { stdout } = await execAsync(detectionCommand, { timeout: 3000, // Reduced timeout for faster detection shell: isWindows ? undefined : '/bin/bash', }); - // The output may contain multiple lines - // We want the first line which should be the actual path + // Output may contain multiple lines, get first line as actual path const lines = stdout .trim() .split('\n') .filter((line) => line.trim()); - const cliPath = lines[0]; // Just take the first path + 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, cliPath, - // Version is not retrieved in lightweight detection + // Version information not retrieved in lightweight detection }; this.lastCheckTime = now; return this.cachedResult; } catch (detectionError) { console.log('[CliDetector] CLI not found, error:', detectionError); - // CLI not found - let error = `Qwen Code CLI not found in PATH. Please install it using: npm install -g @qwen-code/qwen-code@latest`; + + // CLI not found, build error message + let error = `Qwen Code CLI not found in PATH. Please install using: npm install -g @qwen-code/qwen-code@latest`; // Provide specific guidance for permission errors if (detectionError instanceof Error) { @@ -100,11 +121,11 @@ export class CliDetector { errorMessage.includes('EACCES') || errorMessage.includes('Permission denied') ) { - error += `\n\nThis may be due to permission issues. Possible solutions: - \n1. Reinstall the CLI without sudo: npm install -g @qwen-code/qwen-code@latest + error += `\n\nThis may be due to permission issues. Solutions: + \n1. Reinstall CLI without sudo: npm install -g @qwen-code/qwen-code@latest \n2. If previously installed with sudo, fix ownership: sudo chown -R $(whoami) $(npm config get prefix)/lib/node_modules/@qwen-code/qwen-code \n3. Use nvm for Node.js version management to avoid permission issues - \n4. Check your PATH environment variable includes npm's global bin directory`; + \n4. Check PATH environment variable includes npm's global bin directory`; } } @@ -127,11 +148,11 @@ export class CliDetector { errorMessage.includes('EACCES') || errorMessage.includes('Permission denied') ) { - userFriendlyError += `\n\nThis may be due to permission issues. Possible solutions: - \n1. Reinstall the CLI without sudo: npm install -g @qwen-code/qwen-code@latest + userFriendlyError += `\n\nThis may be due to permission issues. Solutions: + \n1. Reinstall CLI without sudo: npm install -g @qwen-code/qwen-code@latest \n2. If previously installed with sudo, fix ownership: sudo chown -R $(whoami) $(npm config get prefix)/lib/node_modules/@qwen-code/qwen-code \n3. Use nvm for Node.js version management to avoid permission issues - \n4. Check your PATH environment variable includes npm's global bin directory`; + \n4. Check PATH environment variable includes npm's global bin directory`; } this.cachedResult = { diff --git a/packages/vscode-ide-companion/src/cli/cliVersionChecker.ts b/packages/vscode-ide-companion/src/cli/cliVersionChecker.ts new file mode 100644 index 00000000..3a9db333 --- /dev/null +++ b/packages/vscode-ide-companion/src/cli/cliVersionChecker.ts @@ -0,0 +1,78 @@ +/** + * @license + * Copyright 2025 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode'; +import { CliContextManager } from './cliContextManager.js'; +import { CliVersionManager } from './cliVersionManager.js'; +import { MIN_CLI_VERSION_FOR_SESSION_METHODS } from './cliVersionManager.js'; +import type { CliVersionInfo } from './cliVersionManager.js'; + +/** + * Check CLI version and show warning if below minimum requirement + * + * @returns Version information + */ +export async function checkCliVersionAndWarn(): Promise { + try { + const cliContextManager = CliContextManager.getInstance(); + const versionInfo = + await CliVersionManager.getInstance().detectCliVersion(true); + cliContextManager.setCurrentVersionInfo(versionInfo); + + if (!versionInfo.isSupported) { + 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.`, + ); + } + } catch (error) { + console.error('[CliVersionChecker] Failed to check CLI version:', 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), + }, + }; + + cliContextManager.setCurrentVersionInfo(versionInfo); + } + } catch (error) { + console.error( + '[CliVersionChecker] Failed to process server version:', + error, + ); + } +} diff --git a/packages/vscode-ide-companion/src/services/acpConnection.ts b/packages/vscode-ide-companion/src/services/acpConnection.ts index f999c602..f4c95948 100644 --- a/packages/vscode-ide-companion/src/services/acpConnection.ts +++ b/packages/vscode-ide-companion/src/services/acpConnection.ts @@ -42,9 +42,7 @@ export class AcpConnection { onPermissionRequest: (data: AcpPermissionRequest) => Promise<{ optionId: string; }> = () => Promise.resolve({ optionId: 'allow' }); - onEndTurn: (reason?: string) => void = (reason?: string | undefined) => { - console.log('[ACP] onEndTurn__________ reason:', reason || 'unknown'); - }; + onEndTurn: () => void = () => {}; // Called after successful initialize() with the initialize result onInitialized: (init: unknown) => void = () => {}; diff --git a/packages/vscode-ide-companion/src/services/acpMessageHandler.ts b/packages/vscode-ide-companion/src/services/acpMessageHandler.ts index e07cedfb..7e4a0ef0 100644 --- a/packages/vscode-ide-companion/src/services/acpMessageHandler.ts +++ b/packages/vscode-ide-companion/src/services/acpMessageHandler.ts @@ -111,16 +111,6 @@ export class AcpMessageHandler { message.result, ); - console.log( - '[ACP] Response for message.result:', - message.result, - message.result && - typeof message.result === 'object' && - 'stopReason' in message.result, - - !!callbacks.onEndTurn, - ); - if (message.result && typeof message.result === 'object') { const stopReasonValue = (message.result as { stopReason?: unknown }).stopReason ?? diff --git a/packages/vscode-ide-companion/src/services/qwenAgentManager.ts b/packages/vscode-ide-companion/src/services/qwenAgentManager.ts index 3c701331..a8bb61fe 100644 --- a/packages/vscode-ide-companion/src/services/qwenAgentManager.ts +++ b/packages/vscode-ide-companion/src/services/qwenAgentManager.ts @@ -24,10 +24,8 @@ import { import { QwenSessionUpdateHandler } from './qwenSessionUpdateHandler.js'; import { CliContextManager } from '../cli/cliContextManager.js'; import { authMethod } from '../types/acpTypes.js'; -import { - MIN_CLI_VERSION_FOR_SESSION_METHODS, - type CliVersionInfo, -} from '../cli/cliVersionManager.js'; +import { MIN_CLI_VERSION_FOR_SESSION_METHODS } from '../cli/cliVersionManager.js'; +import { processServerVersion } from '../cli/cliVersionChecker.js'; import { isAuthenticationRequiredError } from '../utils/authErrors.js'; export type { ChatMessage, PlanEntry, ToolCallUpdateData }; @@ -149,37 +147,10 @@ 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; - - // Extract version information from initialize response - const serverVersion = - obj['version'] || obj['serverVersion'] || obj['cliVersion']; - if (serverVersion) { - console.log( - '[QwenAgentManager] 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), - }, - }; - - cliContextManager.setCurrentVersionInfo(versionInfo); - } - const modes = obj['modes'] as | { currentModeId?: 'plan' | 'default' | 'auto-edit' | 'yolo'; @@ -1369,7 +1340,6 @@ export class QwenAgentManager { * @param callback - Called when ACP stopReason is reported */ onEndTurn(callback: (reason?: string) => void): void { - console.log('[QwenAgentManager] onEndTurn__________ callback:', callback); this.callbacks.onEndTurn = callback; this.sessionUpdateHandler.updateCallbacks(this.callbacks); } diff --git a/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts b/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts index e35ce64c..2fbdd406 100644 --- a/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts +++ b/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts @@ -10,12 +10,12 @@ * Handles Qwen Agent connection establishment, authentication, and session creation */ -// import * as vscode from 'vscode'; import type { AcpConnection } from './acpConnection.js'; 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; @@ -59,19 +59,12 @@ export class QwenConnectionHandler { } console.log('[QwenAgentManager] CLI detected at:', detectionResult.cliPath); - // TODO: @yiliang114. closed temporarily // Show warning if CLI version is below minimum requirement - // if (!versionInfo.isSupported) { - // // Wait to determine release version number - // 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.`, - // ); - // } + await checkCliVersionAndWarn(); // Build extra CLI arguments (only essential parameters) const extraArgs: string[] = []; - // TODO: await connection.connect(cliPath!, workingDir, extraArgs); // Try to restore existing session or create new session diff --git a/packages/vscode-ide-companion/src/utils/authErrors.ts b/packages/vscode-ide-companion/src/utils/authErrors.ts index 4b7de266..d598d8cc 100644 --- a/packages/vscode-ide-companion/src/utils/authErrors.ts +++ b/packages/vscode-ide-companion/src/utils/authErrors.ts @@ -4,23 +4,48 @@ * SPDX-License-Identifier: Apache-2.0 */ +/** + * Authentication Error Utility + * + * Used to uniformly identify and handle various authentication-related error messages. + * Determines if re-authentication is needed by matching predefined error patterns. + * + * @param error - The error object or string to check + * @returns true if it's an authentication-related error, false otherwise + */ const AUTH_ERROR_PATTERNS = [ - 'Authentication required', - '(code: -32000)', - 'Unauthorized', - 'Invalid token', - 'Session expired', + 'Authentication required', // Standard authentication request message + '(code: -32000)', // RPC error code -32000 indicates authentication failure + 'Unauthorized', // HTTP unauthorized error + 'Invalid token', // Invalid token + 'Session expired', // Session expired ]; +/** + * Determines if the given error is authentication-related + * + * This function detects various forms of authentication errors, including: + * - Direct error objects + * - String-form error messages + * - Other types of errors converted to strings for pattern matching + * + * @param error - The error object to check, can be an Error instance, string, or other type + * @returns boolean - true if the error is authentication-related, false otherwise + */ export const isAuthenticationRequiredError = (error: unknown): boolean => { + // Null check to avoid unnecessary processing if (!error) { return false; } + + // Extract error message text const message = error instanceof Error ? error.message : typeof error === 'string' ? error : String(error); + + // Match authentication-related errors using predefined patterns return AUTH_ERROR_PATTERNS.some((pattern) => message.includes(pattern)); }; diff --git a/packages/vscode-ide-companion/src/webview/App.tsx b/packages/vscode-ide-companion/src/webview/App.tsx index d083740c..ca9b86b4 100644 --- a/packages/vscode-ide-companion/src/webview/App.tsx +++ b/packages/vscode-ide-companion/src/webview/App.tsx @@ -29,7 +29,7 @@ import { PermissionDrawer } from './components/PermissionDrawer/PermissionDrawer import { ToolCall } from './components/messages/toolcalls/ToolCall.js'; import { hasToolCallOutput } from './components/messages/toolcalls/shared/utils.js'; import { EmptyState } from './components/layout/EmptyState.js'; -import { OnboardingPage } from './components/layout/OnboardingPage.js'; +import { Onboarding } from './components/layout/Onboarding.js'; import { type CompletionItem } from '../types/completionItemTypes.js'; import { useCompletionTrigger } from './hooks/useCompletionTrigger.js'; import { ChatHeader } from './components/layout/ChatHeader.js'; @@ -670,7 +670,7 @@ export const App: React.FC = () => { > {!hasContent ? ( isAuthenticated === false ? ( - { vscode.postMessage({ type: 'login', data: {} }); messageHandling.setWaitingForResponse( diff --git a/packages/vscode-ide-companion/src/webview/WebViewProvider.ts b/packages/vscode-ide-companion/src/webview/WebViewProvider.ts index 34e6d24f..fa9506f1 100644 --- a/packages/vscode-ide-companion/src/webview/WebViewProvider.ts +++ b/packages/vscode-ide-companion/src/webview/WebViewProvider.ts @@ -17,6 +17,19 @@ import { getFileName } from './utils/webviewUtils.js'; import { type ApprovalModeValue } from '../types/acpTypes.js'; import { isAuthenticationRequiredError } from '../utils/authErrors.js'; +/** + * WebView Provider Class + * + * Manages the WebView panel lifecycle, agent connection, and message handling. + * Acts as the central coordinator between VS Code extension and WebView UI. + * + * Key responsibilities: + * - WebView panel creation and management + * - Qwen agent connection and session management + * - Message routing between extension and WebView + * - Authentication state handling + * - Permission request processing + */ export class WebViewProvider { private panelManager: PanelManager; private messageHandler: MessageHandler; @@ -122,7 +135,6 @@ export class WebViewProvider { // Setup end-turn handler from ACP stopReason notifications this.agentManager.onEndTurn((reason) => { - console.log(' ============== ', reason); // Ensure WebView exits streaming state even if no explicit streamEnd was emitted elsewhere this.sendMessageToWebView({ type: 'streamEnd', @@ -521,6 +533,12 @@ export class WebViewProvider { /** * Attempt to restore authentication state and initialize connection * This is called when the webview is first shown + * + * This method tries to establish a connection without forcing authentication, + * allowing detection of existing authentication state. If connection fails, + * initializes an empty conversation to allow browsing history. + * + * @returns Promise - Resolves when auth state restoration attempt is complete */ private async attemptAuthStateRestoration(): Promise { try { @@ -550,6 +568,16 @@ 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; diff --git a/packages/vscode-ide-companion/src/webview/components/layout/InputForm.tsx b/packages/vscode-ide-companion/src/webview/components/layout/InputForm.tsx index 997c3f9a..1982b18f 100644 --- a/packages/vscode-ide-companion/src/webview/components/layout/InputForm.tsx +++ b/packages/vscode-ide-companion/src/webview/components/layout/InputForm.tsx @@ -172,7 +172,7 @@ export const InputForm: React.FC = ({
= ({ : 'false' } onInput={(e) => { - if (composerDisabled) { - return; - } const target = e.target as HTMLDivElement; // Filter out zero-width space that we use to maintain height const text = target.textContent?.replace(/\u200B/g, '') || ''; diff --git a/packages/vscode-ide-companion/src/webview/components/layout/OnboardingPage.tsx b/packages/vscode-ide-companion/src/webview/components/layout/Onboarding.tsx similarity index 51% rename from packages/vscode-ide-companion/src/webview/components/layout/OnboardingPage.tsx rename to packages/vscode-ide-companion/src/webview/components/layout/Onboarding.tsx index 945f86a4..5bafcfc3 100644 --- a/packages/vscode-ide-companion/src/webview/components/layout/OnboardingPage.tsx +++ b/packages/vscode-ide-companion/src/webview/components/layout/Onboarding.tsx @@ -1,23 +1,30 @@ -import type React from 'react'; +/** + * @license + * Copyright 2025 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + */ + import { generateIconUrl } from '../../utils/resourceUrl.js'; interface OnboardingPageProps { onLogin: () => void; } -export const OnboardingPage: React.FC = ({ onLogin }) => { +export const Onboarding: React.FC = ({ onLogin }) => { const iconUri = generateIconUrl('icon.png'); return (
+ {/* Application icon container with brand logo and decorative close icon */}
Qwen Code Logo + {/* Decorative close icon for enhanced visual effect */}
= ({ onLogin }) => {
+ {/* Text content area */}

Welcome to Qwen Code @@ -40,40 +48,12 @@ export const OnboardingPage: React.FC = ({ onLogin }) => {

- {/*
-
-

Get Started

-
    -
  • -
    - Understand complex codebases faster -
  • -
  • -
    - Navigate with AI-powered suggestions -
  • -
  • -
    - Transform code with confidence -
  • -
-
- -
*/} - - - {/*
-

- By logging in, you agree to the Terms of Service and Privacy - Policy. -

-
*/}
diff --git a/packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.ts b/packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.ts index a227e470..dc9a33b7 100644 --- a/packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.ts +++ b/packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.ts @@ -329,7 +329,6 @@ export const useWebViewMessages = ({ } } } - console.log('[useWebViewMessages1111]__________ other message:', msg); break; } @@ -351,30 +350,42 @@ export const useWebViewMessages = ({ } case 'streamEnd': { - // Always end local streaming state and collapse any thoughts + // Always end local streaming state and clear thinking state handlers.messageHandling.endStreaming(); handlers.messageHandling.clearThinking(); - // If the stream ended due to explicit user cancel, proactively - // clear the waiting indicator and reset any tracked exec calls. - // This avoids the UI being stuck with the Stop button visible - // after rejecting a permission request. + // If stream ended due to explicit user cancellation, proactively clear + // waiting indicator and reset tracked execution calls. + // This avoids UI getting stuck with Stop button visible after + // rejecting a permission request. try { const reason = ( (message.data as { reason?: string } | undefined)?.reason || '' ).toLowerCase(); + + /** + * Handle different types of stream end reasons: + * - 'user_cancelled': User explicitly cancelled operation + * - 'cancelled': General cancellation + * For these cases, immediately clear all active states + */ if (reason === 'user_cancelled' || reason === 'cancelled') { + // Clear active execution tool call tracking, reset state activeExecToolCallsRef.current.clear(); + // Clear waiting response state to ensure UI returns to normal handlers.messageHandling.clearWaitingForResponse(); break; } } catch (_error) { - // best-effort + // Best-effort handling, errors don't affect main flow } - // Otherwise, clear the generic waiting indicator only if there are - // no active long-running tool calls. If there are still active - // execute/bash/command calls, keep the hint visible. + /** + * For other types of stream end (non-user cancellation): + * Only clear generic waiting indicator when there are no active + * long-running tool calls. If there are still active execute/bash/command + * calls, keep the hint visible. + */ if (activeExecToolCallsRef.current.size === 0) { handlers.messageHandling.clearWaitingForResponse(); } @@ -575,15 +586,21 @@ export const useWebViewMessages = ({ // While long-running tools (e.g., execute/bash/command) are in progress, // surface a lightweight loading indicator and expose the Stop button. try { + const id = (toolCallData.toolCallId || '').toString(); const kind = (toolCallData.kind || '').toString().toLowerCase(); - const isExec = + const isExecKind = kind === 'execute' || kind === 'bash' || kind === 'command'; + // CLI sometimes omits kind in tool_call_update payloads; fall back to + // whether we've already tracked this ID as an exec tool. + const wasTrackedExec = activeExecToolCallsRef.current.has(id); + const isExec = isExecKind || wasTrackedExec; - if (isExec) { - const id = (toolCallData.toolCallId || '').toString(); + if (!isExec || !id) { + break; + } - // Maintain the active set by status - if (status === 'pending' || status === 'in_progress') { + if (status === 'pending' || status === 'in_progress') { + if (isExecKind) { activeExecToolCallsRef.current.add(id); // Build a helpful hint from rawInput @@ -597,14 +614,14 @@ export const useWebViewMessages = ({ } const hint = cmd ? `Running: ${cmd}` : 'Running command...'; handlers.messageHandling.setWaitingForResponse(hint); - } else if (status === 'completed' || status === 'failed') { - activeExecToolCallsRef.current.delete(id); } + } else if (status === 'completed' || status === 'failed') { + activeExecToolCallsRef.current.delete(id); + } - // If no active exec tool remains, clear the waiting message. - if (activeExecToolCallsRef.current.size === 0) { - handlers.messageHandling.clearWaitingForResponse(); - } + // If no active exec tool remains, clear the waiting message. + if (activeExecToolCallsRef.current.size === 0) { + handlers.messageHandling.clearWaitingForResponse(); } } catch (_error) { // Best-effort UI hint; ignore errors From f6f4b24356de7e0d3cce0ebe2df5ef2989e652d8 Mon Sep 17 00:00:00 2001 From: yiliang114 <1204183885@qq.com> Date: Sat, 13 Dec 2025 16:49:43 +0800 Subject: [PATCH 06/18] Optimize CLI version warning to avoid repetitive notifications MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added tracking mechanism to prevent showing the same version warning multiple times - This resolves the issue where users were getting frequent warnings when opening new sessions - Kept the existing cache mechanism for version detection performance 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- .../src/cli/cliVersionChecker.ts | 14 +++++++++++--- .../src/webview/components/layout/EmptyState.tsx | 16 ++++++++++------ 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/packages/vscode-ide-companion/src/cli/cliVersionChecker.ts b/packages/vscode-ide-companion/src/cli/cliVersionChecker.ts index 3a9db333..bb760d62 100644 --- a/packages/vscode-ide-companion/src/cli/cliVersionChecker.ts +++ b/packages/vscode-ide-companion/src/cli/cliVersionChecker.ts @@ -10,6 +10,9 @@ 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 +const warnedVersions = new Set(); + /** * Check CLI version and show warning if below minimum requirement * @@ -23,9 +26,14 @@ export async function checkCliVersionAndWarn(): Promise { cliContextManager.setCurrentVersionInfo(versionInfo); if (!versionInfo.isSupported) { - 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.`, - ); + // Only show warning if we haven't already warned about this specific version + const versionKey = versionInfo.version || 'unknown'; + if (!warnedVersions.has(versionKey)) { + 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.`, + ); + warnedVersions.add(versionKey); + } } } catch (error) { console.error('[CliVersionChecker] Failed to check CLI version:', error); 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 f1b15c4c..4c4a486e 100644 --- a/packages/vscode-ide-companion/src/webview/components/layout/EmptyState.tsx +++ b/packages/vscode-ide-companion/src/webview/components/layout/EmptyState.tsx @@ -27,6 +27,16 @@ export const EmptyState: React.FC = ({ return (
+ {/* Loading overlay */} + {loadingMessage && ( +
+
+
+

{loadingMessage}

+
+
+ )} +
{/* Qwen Logo */}
@@ -39,12 +49,6 @@ export const EmptyState: React.FC = ({
{description}
- {loadingMessage && ( -
- - {loadingMessage} -
- )}
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 07/18] 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 b4ec3df8..20570c74 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 9bf238d7..b5ffaaa6 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 31d5aa52..dd6b3352 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 97fffe15..fbd0b530 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 2fbdd406..32873807 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 8f707f4d..2fe11e83 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 39ba6ff9..b926539d 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 d06c9243..c45ac6af 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 4c4a486e..1b424e24 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} From 4590138a1e21d672904a31aa440ad2ac0dbde9f0 Mon Sep 17 00:00:00 2001 From: yiliang114 <1204183885@qq.com> Date: Sat, 13 Dec 2025 18:28:50 +0800 Subject: [PATCH 08/18] fix(vscode-ide-companion/cli): improve Windows compatibility for shell commands fix(vscode-ide-companion/session): improve timeout configuration for different methods --- packages/vscode-ide-companion/src/cli/cliDetector.ts | 4 ++-- packages/vscode-ide-companion/src/cli/cliInstaller.ts | 2 +- .../src/services/acpSessionManager.ts | 10 ++++++++-- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/vscode-ide-companion/src/cli/cliDetector.ts b/packages/vscode-ide-companion/src/cli/cliDetector.ts index 20570c74..3fb8c454 100644 --- a/packages/vscode-ide-companion/src/cli/cliDetector.ts +++ b/packages/vscode-ide-companion/src/cli/cliDetector.ts @@ -186,7 +186,7 @@ export class CliDetector { const { stdout } = await execAsync(detectionCommand, { timeout: 5000, - shell: '/bin/bash', + shell: isWindows ? undefined : '/bin/bash', }); // The output may contain multiple lines, with NVM activation messages // We want the last line which should be the actual path @@ -216,7 +216,7 @@ export class CliDetector { const { stdout: versionOutput } = await execAsync(versionCommand, { timeout: 5000, - shell: '/bin/bash', + shell: isWindows ? undefined : '/bin/bash', }); // The output may contain multiple lines, with NVM activation messages // We want the last line which should be the actual version diff --git a/packages/vscode-ide-companion/src/cli/cliInstaller.ts b/packages/vscode-ide-companion/src/cli/cliInstaller.ts index 4eb0d0e7..8bbf4f51 100644 --- a/packages/vscode-ide-companion/src/cli/cliInstaller.ts +++ b/packages/vscode-ide-companion/src/cli/cliInstaller.ts @@ -124,7 +124,7 @@ export class CliInstaller { installCommand, { timeout: 120000, - shell: '/bin/bash', + shell: process.platform === 'win32' ? undefined : '/bin/bash', }, // 2 minutes timeout ); diff --git a/packages/vscode-ide-companion/src/services/acpSessionManager.ts b/packages/vscode-ide-companion/src/services/acpSessionManager.ts index 55b1d2b5..d313cba7 100644 --- a/packages/vscode-ide-companion/src/services/acpSessionManager.ts +++ b/packages/vscode-ide-companion/src/services/acpSessionManager.ts @@ -54,8 +54,14 @@ export class AcpSessionManager { }; return new Promise((resolve, reject) => { - const timeoutDuration = - method === AGENT_METHODS.session_prompt ? 120000 : 60000; + // different timeout durations based on methods + let timeoutDuration = 60000; // default 60 seconds + if ( + method === AGENT_METHODS.session_prompt || + method === AGENT_METHODS.initialize + ) { + timeoutDuration = 120000; // 2min for session_prompt and initialize + } const timeoutId = setTimeout(() => { pendingRequests.delete(id); From 389d8dd9c482897ff08df945cc383beac993cb23 Mon Sep 17 00:00:00 2001 From: yiliang114 <1204183885@qq.com> Date: Sat, 13 Dec 2025 19:49:04 +0800 Subject: [PATCH 09/18] Remove CLI version checker and status bar display, revert to original notification approach This change removes the CliVersionChecker class and all related status bar functionality, reverting to the original approach that uses vscode.window.showInformationMessage for version-related notifications, as was implemented in the main branch. The changes include: 1. Removing CliVersionChecker import 2. Removing status bar item creation and update logic 3. Removing CLI version check on activation 4. Removing showCliVersionInfo command This addresses the issue where version detection notifications should use vscode.window.showInformationMessage instead of status bar display. --- .../vscode-ide-companion/src/extension.ts | 59 ------------------- 1 file changed, 59 deletions(-) diff --git a/packages/vscode-ide-companion/src/extension.ts b/packages/vscode-ide-companion/src/extension.ts index 24baa957..2adfaef1 100644 --- a/packages/vscode-ide-companion/src/extension.ts +++ b/packages/vscode-ide-companion/src/extension.ts @@ -16,7 +16,6 @@ import { } from '@qwen-code/qwen-code-core/src/ide/detect-ide.js'; import { WebViewProvider } from './webview/WebViewProvider.js'; import { registerNewCommands } from './commands/index.js'; -import { CliVersionChecker } from './cli/cliVersionChecker.js'; const CLI_IDE_COMPANION_IDENTIFIER = 'qwenlm.qwen-code-vscode-ide-companion'; const INFO_MESSAGE_SHOWN_KEY = 'qwenCodeInfoMessageShown'; @@ -104,33 +103,6 @@ async function checkForUpdates( } } -/** - * Update status bar item with CLI version information - */ -async function updateStatusBarItem( - statusBarItem: vscode.StatusBarItem, - context: vscode.ExtensionContext, -) { - try { - // Initialize the version checker - const versionChecker = CliVersionChecker.getInstance(context); - - // Get version status for display - const versionStatus = await versionChecker.getVersionStatus(); - - // Update status bar item - statusBarItem.text = versionStatus; - statusBarItem.tooltip = 'Qwen Code CLI Version Information'; - statusBarItem.show(); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - log(`Error updating status bar: ${message}`); - statusBarItem.text = 'CLI: Error'; - statusBarItem.tooltip = 'Failed to get CLI version information'; - statusBarItem.show(); - } -} - export async function activate(context: vscode.ExtensionContext) { logger = vscode.window.createOutputChannel('Qwen Code Companion'); log = createLogger(context, logger); @@ -138,17 +110,6 @@ export async function activate(context: vscode.ExtensionContext) { checkForUpdates(context, log); - // Create status bar item for CLI version info - const versionStatusBarItem = vscode.window.createStatusBarItem( - vscode.StatusBarAlignment.Right, - 100, - ); - versionStatusBarItem.command = 'qwen-code.showCliVersionInfo'; - context.subscriptions.push(versionStatusBarItem); - - // Update status bar with CLI version info - updateStatusBarItem(versionStatusBarItem, context); - const diffContentProvider = new DiffContentProvider(); const diffManager = new DiffManager( log, @@ -306,26 +267,6 @@ export async function activate(context: vscode.ExtensionContext) { vscode.workspace.onDidGrantWorkspaceTrust(() => { ideServer.syncEnvVars(); }), - vscode.commands.registerCommand( - 'qwen-code.showCliVersionInfo', - async () => { - try { - const versionChecker = CliVersionChecker.getInstance(context); - const versionStatus = await versionChecker.getVersionStatus(); - - // Show information message with version details - vscode.window.showInformationMessage( - `Qwen Code CLI Version: ${versionStatus}`, - ); - } catch (error) { - const message = - error instanceof Error ? error.message : String(error); - vscode.window.showErrorMessage( - `Failed to get CLI version info: ${message}`, - ); - } - }, - ), vscode.commands.registerCommand( 'qwen-code.runQwenCode', async ( 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 10/18] 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 */}
From 61ce586117cd5c95479d5fb786133a2a2fe2922c Mon Sep 17 00:00:00 2001 From: yiliang114 <1204183885@qq.com> Date: Sat, 13 Dec 2025 20:42:59 +0800 Subject: [PATCH 11/18] refactor(vscode-ide-companion/cli): consolidate CLI detection and version management - Replace separate CliDetector, CliVersionChecker, and CliVersionManager classes with unified CliManager - Remove redundant code and simplify CLI detection logic - Maintain all existing functionality while improving code organization - Update imports in dependent files to use CliManager This change reduces complexity by consolidating CLI-related functionality into a single manager class. --- .../src/cli/cliContextManager.ts | 2 +- .../src/cli/cliDetector.ts | 332 ------------ .../src/cli/cliInstaller.ts | 10 +- .../src/cli/cliManager.ts | 498 ++++++++++++++++++ .../src/cli/cliVersionChecker.ts | 133 ----- .../src/cli/cliVersionManager.ts | 191 ------- .../src/extension.test.ts | 12 - .../src/services/qwenAgentManager.ts | 2 +- .../src/services/qwenConnectionHandler.ts | 4 +- .../src/utils/authErrors.ts | 17 - .../src/utils/authNotificationHandler.ts | 33 +- .../src/webview/WebViewProvider.ts | 8 +- .../messages/Assistant/AssistantMessage.tsx | 6 +- 13 files changed, 527 insertions(+), 721 deletions(-) delete mode 100644 packages/vscode-ide-companion/src/cli/cliDetector.ts create mode 100644 packages/vscode-ide-companion/src/cli/cliManager.ts delete mode 100644 packages/vscode-ide-companion/src/cli/cliVersionChecker.ts delete mode 100644 packages/vscode-ide-companion/src/cli/cliVersionManager.ts diff --git a/packages/vscode-ide-companion/src/cli/cliContextManager.ts b/packages/vscode-ide-companion/src/cli/cliContextManager.ts index c812a08e..b13798b8 100644 --- a/packages/vscode-ide-companion/src/cli/cliContextManager.ts +++ b/packages/vscode-ide-companion/src/cli/cliContextManager.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import type { CliFeatureFlags, CliVersionInfo } from './cliVersionManager.js'; +import type { CliFeatureFlags, CliVersionInfo } from './cliManager.js'; export class CliContextManager { private static instance: CliContextManager; diff --git a/packages/vscode-ide-companion/src/cli/cliDetector.ts b/packages/vscode-ide-companion/src/cli/cliDetector.ts deleted file mode 100644 index 3fb8c454..00000000 --- a/packages/vscode-ide-companion/src/cli/cliDetector.ts +++ /dev/null @@ -1,332 +0,0 @@ -/** - * @license - * Copyright 2025 Qwen Team - * SPDX-License-Identifier: Apache-2.0 - */ - -import { exec } from 'child_process'; -import { promisify } from 'util'; - -const execAsync = promisify(exec); - -export interface CliDetectionResult { - isInstalled: boolean; - cliPath?: string; - version?: string; - error?: string; -} - -/** - * Detects if Qwen Code CLI is installed and accessible - */ -export class CliDetector { - private static cachedResult: CliDetectionResult | null = null; - private static lastCheckTime: number = 0; - private static readonly CACHE_DURATION_MS = 30000; // 30 seconds - - /** - * Lightweight CLI Detection Method - * - * This method is designed for performance optimization, checking only if the CLI exists - * without retrieving version information. - * Suitable for quick detection scenarios, such as pre-checks before initializing connections. - * - * Compared to the full detectQwenCli method, this method: - * - Omits version information retrieval step - * - Uses shorter timeout (3 seconds) - * - Faster response time - * - * @param forceRefresh - Whether to force refresh cached results, default is false - * @returns Promise - Detection result containing installation status and path - */ - static async detectQwenCliLightweight( - forceRefresh = false, - ): Promise { - const now = Date.now(); - - // Check if cached result is available and not expired (30-second validity) - if ( - !forceRefresh && - this.cachedResult && - now - this.lastCheckTime < this.CACHE_DURATION_MS - ) { - return this.cachedResult; - } - - try { - const isWindows = process.platform === 'win32'; - const whichCommand = isWindows ? 'where' : 'which'; - - // Check if qwen command exists - try { - // Use simplified detection without NVM for speed - const detectionCommand = isWindows - ? `${whichCommand} qwen` - : `${whichCommand} qwen`; - - // Execute command to detect CLI path, set shorter timeout (3 seconds) - const { stdout } = await execAsync(detectionCommand, { - timeout: 3000, // Reduced timeout for faster detection - shell: isWindows ? undefined : '/bin/bash', - }); - - // Output may contain multiple lines, get first line as actual path - const lines = stdout - .trim() - .split('\n') - .filter((line) => line.trim()); - const cliPath = lines[0]; // Take only the first path - - // Build successful detection result, note no version information - this.cachedResult = { - isInstalled: true, - cliPath, - // Version information not retrieved in lightweight detection - }; - this.lastCheckTime = now; - return this.cachedResult; - } catch (detectionError) { - console.log('[CliDetector] CLI not found, error:', detectionError); - - // CLI not found, build error message - let error = `Qwen Code CLI not found in PATH. Please install using: npm install -g @qwen-code/qwen-code@latest`; - - // Provide specific guidance for permission errors - if (detectionError instanceof Error) { - const errorMessage = detectionError.message; - if ( - errorMessage.includes('EACCES') || - errorMessage.includes('Permission denied') - ) { - error += `\n\nThis may be due to permission issues. Solutions: - \n1. Reinstall CLI without sudo: npm install -g @qwen-code/qwen-code@latest - \n2. If previously installed with sudo, fix ownership: sudo chown -R $(whoami) $(npm config get prefix)/lib/node_modules/@qwen-code/qwen-code - \n3. Use nvm for Node.js version management to avoid permission issues - \n4. Check PATH environment variable includes npm's global bin directory`; - } - } - - this.cachedResult = { - isInstalled: false, - error, - }; - this.lastCheckTime = now; - return this.cachedResult; - } - } catch (error) { - console.log('[CliDetector] General detection error:', error); - const errorMessage = - error instanceof Error ? error.message : String(error); - - let userFriendlyError = `Failed to detect Qwen Code CLI: ${errorMessage}`; - - // Provide specific guidance for permission errors - if ( - errorMessage.includes('EACCES') || - errorMessage.includes('Permission denied') - ) { - userFriendlyError += `\n\nThis may be due to permission issues. Solutions: - \n1. Reinstall CLI without sudo: npm install -g @qwen-code/qwen-code@latest - \n2. If previously installed with sudo, fix ownership: sudo chown -R $(whoami) $(npm config get prefix)/lib/node_modules/@qwen-code/qwen-code - \n3. Use nvm for Node.js version management to avoid permission issues - \n4. Check PATH environment variable includes npm's global bin directory`; - } - - this.cachedResult = { - isInstalled: false, - error: userFriendlyError, - }; - this.lastCheckTime = now; - return this.cachedResult; - } - } - - /** - * Checks if the Qwen Code CLI is installed - * @param forceRefresh - Force a new check, ignoring cache - * @returns Detection result with installation status and details - */ - static async detectQwenCli( - forceRefresh = false, - ): Promise { - const now = Date.now(); - - // Return cached result if available and not expired - if ( - !forceRefresh && - this.cachedResult && - now - this.lastCheckTime < this.CACHE_DURATION_MS - ) { - console.log('[CliDetector] Returning cached result'); - return this.cachedResult; - } - - console.log( - '[CliDetector] Starting CLI detection, current PATH:', - process.env.PATH, - ); - - try { - const isWindows = process.platform === 'win32'; - const whichCommand = isWindows ? 'where' : 'which'; - - // Check if qwen command exists - try { - // Use NVM environment for consistent detection - // Fallback chain: default alias -> node alias -> current version - const detectionCommand = - process.platform === 'win32' - ? `${whichCommand} qwen` - : 'source ~/.nvm/nvm.sh 2>/dev/null && (nvm use default 2>/dev/null || nvm use node 2>/dev/null || nvm use 2>/dev/null); which qwen'; - - console.log( - '[CliDetector] Detecting CLI with command:', - detectionCommand, - ); - - const { stdout } = await execAsync(detectionCommand, { - timeout: 5000, - shell: isWindows ? undefined : '/bin/bash', - }); - // The output may contain multiple lines, with NVM activation messages - // We want the last line which should be the actual path - const lines = stdout - .trim() - .split('\n') - .filter((line) => line.trim()); - const cliPath = lines[lines.length - 1]; - - console.log('[CliDetector] Found CLI at:', cliPath); - - // Try to get version - let version: string | undefined; - try { - // Use NVM environment for version check - // Fallback chain: default alias -> node alias -> current version - // Also ensure we use the correct Node.js version that matches the CLI installation - const versionCommand = - process.platform === 'win32' - ? 'qwen --version' - : 'source ~/.nvm/nvm.sh 2>/dev/null && (nvm use default 2>/dev/null || nvm use node 2>/dev/null || nvm use 2>/dev/null); qwen --version'; - - console.log( - '[CliDetector] Getting version with command:', - versionCommand, - ); - - const { stdout: versionOutput } = await execAsync(versionCommand, { - timeout: 5000, - shell: isWindows ? undefined : '/bin/bash', - }); - // The output may contain multiple lines, with NVM activation messages - // We want the last line which should be the actual version - const versionLines = versionOutput - .trim() - .split('\n') - .filter((line) => line.trim()); - version = versionLines[versionLines.length - 1]; - console.log('[CliDetector] CLI version:', version); - } catch (versionError) { - console.log('[CliDetector] Failed to get CLI version:', versionError); - // Version check failed, but CLI is installed - } - - this.cachedResult = { - isInstalled: true, - cliPath, - version, - }; - this.lastCheckTime = now; - return this.cachedResult; - } catch (detectionError) { - console.log('[CliDetector] CLI not found, error:', detectionError); - // CLI not found - let error = `Qwen Code CLI not found in PATH. Please install it using: npm install -g @qwen-code/qwen-code@latest`; - - // Provide specific guidance for permission errors - if (detectionError instanceof Error) { - const errorMessage = detectionError.message; - if ( - errorMessage.includes('EACCES') || - errorMessage.includes('Permission denied') - ) { - error += `\n\nThis may be due to permission issues. Possible solutions: - \n1. Reinstall the CLI without sudo: npm install -g @qwen-code/qwen-code@latest - \n2. If previously installed with sudo, fix ownership: sudo chown -R $(whoami) $(npm config get prefix)/lib/node_modules/@qwen-code/qwen-code - \n3. Use nvm for Node.js version management to avoid permission issues - \n4. Check your PATH environment variable includes npm's global bin directory`; - } - } - - this.cachedResult = { - isInstalled: false, - error, - }; - this.lastCheckTime = now; - return this.cachedResult; - } - } catch (error) { - console.log('[CliDetector] General detection error:', error); - const errorMessage = - error instanceof Error ? error.message : String(error); - - let userFriendlyError = `Failed to detect Qwen Code CLI: ${errorMessage}`; - - // Provide specific guidance for permission errors - if ( - errorMessage.includes('EACCES') || - errorMessage.includes('Permission denied') - ) { - userFriendlyError += `\n\nThis may be due to permission issues. Possible solutions: - \n1. Reinstall the CLI without sudo: npm install -g @qwen-code/qwen-code@latest - \n2. If previously installed with sudo, fix ownership: sudo chown -R $(whoami) $(npm config get prefix)/lib/node_modules/@qwen-code/qwen-code - \n3. Use nvm for Node.js version management to avoid permission issues - \n4. Check your PATH environment variable includes npm's global bin directory`; - } - - this.cachedResult = { - isInstalled: false, - error: userFriendlyError, - }; - this.lastCheckTime = now; - return this.cachedResult; - } - } - - /** - * Clears the cached detection result - */ - static clearCache(): void { - this.cachedResult = null; - this.lastCheckTime = 0; - } - - /** - * Gets installation instructions based on the platform - */ - static getInstallationInstructions(): { - title: string; - steps: string[]; - documentationUrl: string; - } { - return { - title: 'Qwen Code CLI is not installed', - steps: [ - 'Install via npm:', - ' npm install -g @qwen-code/qwen-code@latest', - '', - 'If you are using nvm (automatically handled by the plugin):', - ' The plugin will automatically use your default nvm version', - '', - 'Or install from source:', - ' git clone https://github.com/QwenLM/qwen-code.git', - ' cd qwen-code', - ' npm install', - ' npm install -g .', - '', - 'After installation, reload VS Code or restart the extension.', - ], - documentationUrl: 'https://github.com/QwenLM/qwen-code#installation', - }; - } -} diff --git a/packages/vscode-ide-companion/src/cli/cliInstaller.ts b/packages/vscode-ide-companion/src/cli/cliInstaller.ts index 8bbf4f51..7ad46f06 100644 --- a/packages/vscode-ide-companion/src/cli/cliInstaller.ts +++ b/packages/vscode-ide-companion/src/cli/cliInstaller.ts @@ -5,7 +5,7 @@ */ import * as vscode from 'vscode'; -import { CliDetector } from './cliDetector.js'; +import { CliManager } from './cliManager.js'; /** * CLI Detection and Installation Handler @@ -20,7 +20,7 @@ export class CliInstaller { sendToWebView: (message: unknown) => void, ): Promise { try { - const result = await CliDetector.detectQwenCli(); + const result = await CliManager.detectQwenCli(); sendToWebView({ type: 'cliDetectionResult', @@ -31,7 +31,7 @@ export class CliInstaller { error: result.error, installInstructions: result.isInstalled ? undefined - : CliDetector.getInstallationInstructions(), + : CliManager.getInstallationInstructions(), }, }); @@ -134,8 +134,8 @@ export class CliInstaller { } // Clear cache and recheck - CliDetector.clearCache(); - const detection = await CliDetector.detectQwenCli(); + CliManager.clearCache(); + const detection = await CliManager.detectQwenCli(); if (detection.isInstalled) { vscode.window diff --git a/packages/vscode-ide-companion/src/cli/cliManager.ts b/packages/vscode-ide-companion/src/cli/cliManager.ts new file mode 100644 index 00000000..a11fe668 --- /dev/null +++ b/packages/vscode-ide-companion/src/cli/cliManager.ts @@ -0,0 +1,498 @@ +/** + * @license + * Copyright 2025 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode'; +import { exec } from 'child_process'; +import { promisify } from 'util'; +import semver from 'semver'; +import { CliInstaller } from './cliInstaller.js'; + +const execAsync = promisify(exec); + +export const MIN_CLI_VERSION_FOR_SESSION_METHODS = '0.5.0'; + +export interface CliDetectionResult { + isInstalled: boolean; + cliPath?: string; + version?: string; + error?: string; +} + +export interface CliFeatureFlags { + supportsSessionList: boolean; + supportsSessionLoad: boolean; +} + +export interface CliVersionInfo { + version: string | undefined; + isSupported: boolean; + features: CliFeatureFlags; + detectionResult: CliDetectionResult; +} + +export class CliManager { + private static instance: CliManager; + private lastNotificationTime: number = 0; + private static readonly NOTIFICATION_COOLDOWN_MS = 300000; // 5 minutes cooldown + private context: vscode.ExtensionContext | undefined; + + // Cache mechanisms + private static cachedDetectionResult: CliDetectionResult | null = null; + private static detectionLastCheckTime: number = 0; + private cachedVersionInfo: CliVersionInfo | null = null; + private versionLastCheckTime: number = 0; + private static readonly CACHE_DURATION_MS = 30000; // 30 seconds + + private constructor(context?: vscode.ExtensionContext) { + this.context = context; + } + + /** + * Get singleton instance + */ + static getInstance(context?: vscode.ExtensionContext): CliManager { + if (!CliManager.instance && context) { + CliManager.instance = new CliManager(context); + } + return CliManager.instance; + } + + /** + * Checks if the Qwen Code CLI is installed + * @param forceRefresh - Force a new check, ignoring cache + * @returns Detection result with installation status and details + */ + static async detectQwenCli( + forceRefresh = false, + ): Promise { + const now = Date.now(); + + // Return cached result if available and not expired + if ( + !forceRefresh && + this.cachedDetectionResult && + now - this.detectionLastCheckTime < this.CACHE_DURATION_MS + ) { + console.log('[CliManager] Returning cached detection result'); + return this.cachedDetectionResult; + } + + console.log( + '[CliManager] Starting CLI detection, current PATH:', + process.env.PATH, + ); + + try { + const isWindows = process.platform === 'win32'; + const whichCommand = isWindows ? 'where' : 'which'; + + // Check if qwen command exists + try { + // Use NVM environment for consistent detection + // Fallback chain: default alias -> node alias -> current version + const detectionCommand = + process.platform === 'win32' + ? `${whichCommand} qwen` + : 'source ~/.nvm/nvm.sh 2>/dev/null && (nvm use default 2>/dev/null || nvm use node 2>/dev/null || nvm use 2>/dev/null); which qwen'; + + console.log( + '[CliManager] Detecting CLI with command:', + detectionCommand, + ); + + const { stdout } = await execAsync(detectionCommand, { + timeout: 5000, + shell: isWindows ? undefined : '/bin/bash', + }); + // The output may contain multiple lines, with NVM activation messages + // We want the last line which should be the actual path + const lines = stdout + .trim() + .split('\n') + .filter((line) => line.trim()); + const cliPath = lines[lines.length - 1]; + + console.log('[CliManager] Found CLI at:', cliPath); + + // Try to get version + let version: string | undefined; + try { + // Use NVM environment for version check + // Fallback chain: default alias -> node alias -> current version + // Also ensure we use the correct Node.js version that matches the CLI installation + const versionCommand = + process.platform === 'win32' + ? 'qwen --version' + : 'source ~/.nvm/nvm.sh 2>/dev/null && (nvm use default 2>/dev/null || nvm use node 2>/dev/null || nvm use 2>/dev/null); qwen --version'; + + console.log( + '[CliManager] Getting version with command:', + versionCommand, + ); + + const { stdout: versionOutput } = await execAsync(versionCommand, { + timeout: 5000, + shell: isWindows ? undefined : '/bin/bash', + }); + // The output may contain multiple lines, with NVM activation messages + // We want the last line which should be the actual version + const versionLines = versionOutput + .trim() + .split('\n') + .filter((line) => line.trim()); + version = versionLines[versionLines.length - 1]; + console.log('[CliManager] CLI version:', version); + } catch (versionError) { + console.log('[CliManager] Failed to get CLI version:', versionError); + // Version check failed, but CLI is installed + } + + this.cachedDetectionResult = { + isInstalled: true, + cliPath, + version, + }; + this.detectionLastCheckTime = now; + return this.cachedDetectionResult; + } catch (detectionError) { + console.log('[CliManager] CLI not found, error:', detectionError); + // CLI not found + let error = `Qwen Code CLI not found in PATH. Please install it using: npm install -g @qwen-code/qwen-code@latest`; + + // Provide specific guidance for permission errors + if (detectionError instanceof Error) { + const errorMessage = detectionError.message; + if ( + errorMessage.includes('EACCES') || + errorMessage.includes('Permission denied') + ) { + error += `\n\nThis may be due to permission issues. Possible solutions: + \n1. Reinstall the CLI without sudo: npm install -g @qwen-code/qwen-code@latest + \n2. If previously installed with sudo, fix ownership: sudo chown -R $(whoami) $(npm config get prefix)/lib/node_modules/@qwen-code/qwen-code + \n3. Use nvm for Node.js version management to avoid permission issues + \n4. Check your PATH environment variable includes npm's global bin directory`; + } + } + + this.cachedDetectionResult = { + isInstalled: false, + error, + }; + this.detectionLastCheckTime = now; + return this.cachedDetectionResult; + } + } catch (error) { + console.log('[CliManager] General detection error:', error); + const errorMessage = + error instanceof Error ? error.message : String(error); + + let userFriendlyError = `Failed to detect Qwen Code CLI: ${errorMessage}`; + + // Provide specific guidance for permission errors + if ( + errorMessage.includes('EACCES') || + errorMessage.includes('Permission denied') + ) { + userFriendlyError += `\n\nThis may be due to permission issues. Possible solutions: + \n1. Reinstall the CLI without sudo: npm install -g @qwen-code/qwen-code@latest + \n2. If previously installed with sudo, fix ownership: sudo chown -R $(whoami) $(npm config get prefix)/lib/node_modules/@qwen-code/qwen-code + \n3. Use nvm for Node.js version management to avoid permission issues + \n4. Check your PATH environment variable includes npm's global bin directory`; + } + + this.cachedDetectionResult = { + isInstalled: false, + error: userFriendlyError, + }; + this.detectionLastCheckTime = now; + return this.cachedDetectionResult; + } + } + + /** + * Clears the cached detection result + */ + static clearCache(): void { + this.cachedDetectionResult = null; + this.detectionLastCheckTime = 0; + } + + /** + * Gets installation instructions based on the platform + */ + static getInstallationInstructions(): { + title: string; + steps: string[]; + documentationUrl: string; + } { + return { + title: 'Qwen Code CLI is not installed', + steps: [ + 'Install via npm:', + ' npm install -g @qwen-code/qwen-code@latest', + '', + 'If you are using nvm (automatically handled by the plugin):', + ' The plugin will automatically use your default nvm version', + '', + 'Or install from source:', + ' git clone https://github.com/QwenLM/qwen-code.git', + ' cd qwen-code', + ' npm install', + ' npm install -g .', + '', + 'After installation, reload VS Code or restart the extension.', + ], + documentationUrl: 'https://github.com/QwenLM/qwen-code#installation', + }; + } + + /** + * Check if CLI version meets minimum requirements + * + * @param version - Version string to check + * @param minVersion - Minimum required version + * @returns Whether version meets requirements + */ + private isVersionSupported( + version: string | undefined, + minVersion: string, + ): boolean { + if (!version) { + return false; + } + + // Use semver for robust comparison (handles v-prefix, pre-release, etc.) + const v = semver.valid(version) ?? semver.coerce(version)?.version ?? null; + const min = + semver.valid(minVersion) ?? semver.coerce(minVersion)?.version ?? null; + + if (!v || !min) { + console.warn( + `[CliManager] Invalid semver: version=${version}, min=${minVersion}`, + ); + return false; + } + console.log(`[CliManager] Version ${v} meets requirements: ${min}`); + return semver.gte(v, min); + } + + /** + * Get feature flags based on CLI version + * + * @param version - CLI version string + * @returns Feature flags + */ + private getFeatureFlags(version: string | undefined): CliFeatureFlags { + const isSupportedVersion = this.isVersionSupported( + version, + MIN_CLI_VERSION_FOR_SESSION_METHODS, + ); + + return { + supportsSessionList: isSupportedVersion, + supportsSessionLoad: isSupportedVersion, + }; + } + + /** + * Detect CLI version and features + * + * @param forceRefresh - Force a new check, ignoring cache + * @returns CLI version information + */ + async detectCliVersion(forceRefresh = false): Promise { + const now = Date.now(); + + // Return cached result if available and not expired + if ( + !forceRefresh && + this.cachedVersionInfo && + now - this.versionLastCheckTime < CliManager.CACHE_DURATION_MS + ) { + console.log('[CliManager] Returning cached version info'); + return this.cachedVersionInfo; + } + + console.log('[CliManager] Detecting CLI version...'); + + try { + // Detect CLI installation + const detectionResult = await CliManager.detectQwenCli(forceRefresh); + + const versionInfo: CliVersionInfo = { + version: detectionResult.version, + isSupported: this.isVersionSupported( + detectionResult.version, + MIN_CLI_VERSION_FOR_SESSION_METHODS, + ), + features: this.getFeatureFlags(detectionResult.version), + detectionResult, + }; + + // Cache the result + this.cachedVersionInfo = versionInfo; + this.versionLastCheckTime = now; + + console.log('[CliManager] CLI version detection result:', versionInfo); + + return versionInfo; + } catch (error) { + console.error('[CliManager] Failed to detect CLI version:', error); + + // Return fallback result + const fallbackResult: CliVersionInfo = { + version: undefined, + isSupported: false, + features: { + supportsSessionList: false, + supportsSessionLoad: false, + }, + detectionResult: { + isInstalled: false, + error: error instanceof Error ? error.message : String(error), + }, + }; + + return fallbackResult; + } + } + + /** + * Clear cached version information + */ + clearVersionCache(): void { + this.cachedVersionInfo = null; + this.versionLastCheckTime = 0; + CliManager.clearCache(); + } + + /** + * Check if CLI supports session/list method + * + * @param forceRefresh - Force a new check, ignoring cache + * @returns Whether session/list is supported + */ + async supportsSessionList(forceRefresh = false): Promise { + const versionInfo = await this.detectCliVersion(forceRefresh); + return versionInfo.features.supportsSessionList; + } + + /** + * Check if CLI supports session/load method + * + * @param forceRefresh - Force a new check, ignoring cache + * @returns Whether session/load is supported + */ + async supportsSessionLoad(forceRefresh = false): Promise { + const versionInfo = await this.detectCliVersion(forceRefresh); + return versionInfo.features.supportsSessionLoad; + } + + /** + * 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 CliManager.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`, + ); + this.lastNotificationTime = Date.now(); + } + + return { + isInstalled: false, + error: detectionResult.error, + isSupported: false, + needsUpdate: false, + }; + } + + // Get version information + const versionInfo = await this.detectCliVersion(); + + const currentVersion = detectionResult.version; + const isSupported = versionInfo.isSupported; + + // Check if update is needed (version is too old) + const needsUpdate = currentVersion + ? !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 ${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`, + 'Upgrade Now', + 'View Documentation', + ) + .then(async (selection) => { + if (selection === 'Upgrade Now') { + await CliInstaller.install(); + } else if (selection === 'View Documentation') { + vscode.env.openExternal( + vscode.Uri.parse( + 'https://github.com/QwenLM/qwen-code#installation', + ), + ); + } + }); + this.lastNotificationTime = Date.now(); + } + + return { + isInstalled: true, + version: currentVersion, + isSupported, + needsUpdate, + }; + } catch (error) { + console.error('[CliManager] 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), + isSupported: false, + needsUpdate: false, + }; + } + } + + /** + * Check if notification can be shown based on cooldown period + */ + private canShowNotification(): boolean { + return ( + Date.now() - this.lastNotificationTime > + CliManager.NOTIFICATION_COOLDOWN_MS + ); + } +} diff --git a/packages/vscode-ide-companion/src/cli/cliVersionChecker.ts b/packages/vscode-ide-companion/src/cli/cliVersionChecker.ts deleted file mode 100644 index 6c93609d..00000000 --- a/packages/vscode-ide-companion/src/cli/cliVersionChecker.ts +++ /dev/null @@ -1,133 +0,0 @@ -/** - * @license - * Copyright 2025 Qwen Team - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as vscode from 'vscode'; -import { CliDetector, type CliDetectionResult } from './cliDetector.js'; -import { - CliVersionManager, - MIN_CLI_VERSION_FOR_SESSION_METHODS, -} from './cliVersionManager.js'; -import semver from 'semver'; - -/** - * CLI Version Checker - * - * 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 class CliVersionChecker { - private static instance: CliVersionChecker; - private lastNotificationTime: number = 0; - private static readonly NOTIFICATION_COOLDOWN_MS = 300000; // 5 minutes cooldown - private context: vscode.ExtensionContext; - - private constructor(context: vscode.ExtensionContext) { - this.context = context; - } - - /** - * Get singleton instance - */ - static getInstance(context?: vscode.ExtensionContext): CliVersionChecker { - if (!CliVersionChecker.instance && context) { - CliVersionChecker.instance = new CliVersionChecker(context); - } - return CliVersionChecker.instance; - } - - /** - * 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`, - ); - this.lastNotificationTime = Date.now(); - } - - return { - isInstalled: false, - error: detectionResult.error, - isSupported: false, - needsUpdate: false, - }; - } - - // 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 needsUpdate = currentVersion - ? !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 ${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(); - } - - 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), - isSupported: false, - needsUpdate: false, - }; - } - } - - /** - * Check if notification can be shown based on cooldown period - */ - private canShowNotification(): boolean { - return ( - Date.now() - this.lastNotificationTime > - CliVersionChecker.NOTIFICATION_COOLDOWN_MS - ); - } -} diff --git a/packages/vscode-ide-companion/src/cli/cliVersionManager.ts b/packages/vscode-ide-companion/src/cli/cliVersionManager.ts deleted file mode 100644 index 0cd6ca2c..00000000 --- a/packages/vscode-ide-companion/src/cli/cliVersionManager.ts +++ /dev/null @@ -1,191 +0,0 @@ -/** - * @license - * Copyright 2025 Qwen Team - * SPDX-License-Identifier: Apache-2.0 - */ - -import semver from 'semver'; -import { CliDetector, type CliDetectionResult } from './cliDetector.js'; - -export const MIN_CLI_VERSION_FOR_SESSION_METHODS = '0.5.0'; - -export interface CliFeatureFlags { - supportsSessionList: boolean; - supportsSessionLoad: boolean; -} - -export interface CliVersionInfo { - version: string | undefined; - isSupported: boolean; - features: CliFeatureFlags; - detectionResult: CliDetectionResult; -} - -/** - * CLI Version Manager - * - * Manages CLI version detection and feature availability based on version - */ -export class CliVersionManager { - private static instance: CliVersionManager; - private cachedVersionInfo: CliVersionInfo | null = null; - private lastCheckTime: number = 0; - private static readonly CACHE_DURATION_MS = 30000; // 30 seconds - - private constructor() {} - - /** - * Get singleton instance - */ - static getInstance(): CliVersionManager { - if (!CliVersionManager.instance) { - CliVersionManager.instance = new CliVersionManager(); - } - return CliVersionManager.instance; - } - - /** - * Check if CLI version meets minimum requirements - * - * @param version - Version string to check - * @param minVersion - Minimum required version - * @returns Whether version meets requirements - */ - private isVersionSupported( - version: string | undefined, - minVersion: string, - ): boolean { - if (!version) { - return false; - } - - // Use semver for robust comparison (handles v-prefix, pre-release, etc.) - const v = semver.valid(version) ?? semver.coerce(version)?.version ?? null; - const min = - semver.valid(minVersion) ?? semver.coerce(minVersion)?.version ?? null; - - if (!v || !min) { - console.warn( - `[CliVersionManager] Invalid semver: version=${version}, min=${minVersion}`, - ); - return false; - } - console.log(`[CliVersionManager] Version ${v} meets requirements: ${min}`); - return semver.gte(v, min); - } - - /** - * Get feature flags based on CLI version - * - * @param version - CLI version string - * @returns Feature flags - */ - private getFeatureFlags(version: string | undefined): CliFeatureFlags { - const isSupportedVersion = this.isVersionSupported( - version, - MIN_CLI_VERSION_FOR_SESSION_METHODS, - ); - - return { - supportsSessionList: isSupportedVersion, - supportsSessionLoad: isSupportedVersion, - }; - } - - /** - * Detect CLI version and features - * - * @param forceRefresh - Force a new check, ignoring cache - * @returns CLI version information - */ - async detectCliVersion(forceRefresh = false): Promise { - const now = Date.now(); - - // Return cached result if available and not expired - if ( - !forceRefresh && - this.cachedVersionInfo && - now - this.lastCheckTime < CliVersionManager.CACHE_DURATION_MS - ) { - console.log('[CliVersionManager] Returning cached version info'); - return this.cachedVersionInfo; - } - - console.log('[CliVersionManager] Detecting CLI version...'); - - try { - // Detect CLI installation - const detectionResult = await CliDetector.detectQwenCli(forceRefresh); - - const versionInfo: CliVersionInfo = { - version: detectionResult.version, - isSupported: this.isVersionSupported( - detectionResult.version, - MIN_CLI_VERSION_FOR_SESSION_METHODS, - ), - features: this.getFeatureFlags(detectionResult.version), - detectionResult, - }; - - // Cache the result - this.cachedVersionInfo = versionInfo; - this.lastCheckTime = now; - - console.log( - '[CliVersionManager] CLI version detection result:', - versionInfo, - ); - - return versionInfo; - } catch (error) { - console.error('[CliVersionManager] Failed to detect CLI version:', error); - - // Return fallback result - const fallbackResult: CliVersionInfo = { - version: undefined, - isSupported: false, - features: { - supportsSessionList: false, - supportsSessionLoad: false, - }, - detectionResult: { - isInstalled: false, - error: error instanceof Error ? error.message : String(error), - }, - }; - - return fallbackResult; - } - } - - /** - * Clear cached version information - */ - clearCache(): void { - this.cachedVersionInfo = null; - this.lastCheckTime = 0; - CliDetector.clearCache(); - } - - /** - * Check if CLI supports session/list method - * - * @param forceRefresh - Force a new check, ignoring cache - * @returns Whether session/list is supported - */ - async supportsSessionList(forceRefresh = false): Promise { - const versionInfo = await this.detectCliVersion(forceRefresh); - return versionInfo.features.supportsSessionList; - } - - /** - * Check if CLI supports session/load method - * - * @param forceRefresh - Force a new check, ignoring cache - * @returns Whether session/load is supported - */ - async supportsSessionLoad(forceRefresh = false): Promise { - const versionInfo = await this.detectCliVersion(forceRefresh); - return versionInfo.features.supportsSessionLoad; - } -} diff --git a/packages/vscode-ide-companion/src/extension.test.ts b/packages/vscode-ide-companion/src/extension.test.ts index dd6b3352..31d5aa52 100644 --- a/packages/vscode-ide-companion/src/extension.test.ts +++ b/packages/vscode-ide-companion/src/extension.test.ts @@ -43,14 +43,6 @@ 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: [], @@ -66,10 +58,6 @@ 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 d7804ab4..6624d71e 100644 --- a/packages/vscode-ide-companion/src/services/qwenAgentManager.ts +++ b/packages/vscode-ide-companion/src/services/qwenAgentManager.ts @@ -25,7 +25,7 @@ import { 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 { MIN_CLI_VERSION_FOR_SESSION_METHODS } from '../cli/cliManager.js'; import { isAuthenticationRequiredError } from '../utils/authErrors.js'; import { handleAuthenticateUpdate } from '../utils/authNotificationHandler.js'; diff --git a/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts b/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts index 32873807..7085340e 100644 --- a/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts +++ b/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts @@ -12,7 +12,7 @@ import type { AcpConnection } from './acpConnection.js'; import type { QwenSessionReader } from '../services/qwenSessionReader.js'; -import { CliDetector } from '../cli/cliDetector.js'; +import { CliManager } from '../cli/cliManager.js'; import { authMethod } from '../types/acpTypes.js'; import { isAuthenticationRequiredError } from '../utils/authErrors.js'; @@ -50,7 +50,7 @@ export class QwenConnectionHandler { let requiresAuth = false; // Check if CLI exists using standard detection (with cached results for better performance) - const detectionResult = await CliDetector.detectQwenCli( + const detectionResult = await CliManager.detectQwenCli( /* forceRefresh */ false, // Use cached results when available for better performance ); if (!detectionResult.isInstalled) { diff --git a/packages/vscode-ide-companion/src/utils/authErrors.ts b/packages/vscode-ide-companion/src/utils/authErrors.ts index d598d8cc..8b0e6af9 100644 --- a/packages/vscode-ide-companion/src/utils/authErrors.ts +++ b/packages/vscode-ide-companion/src/utils/authErrors.ts @@ -4,15 +4,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -/** - * Authentication Error Utility - * - * Used to uniformly identify and handle various authentication-related error messages. - * Determines if re-authentication is needed by matching predefined error patterns. - * - * @param error - The error object or string to check - * @returns true if it's an authentication-related error, false otherwise - */ const AUTH_ERROR_PATTERNS = [ 'Authentication required', // Standard authentication request message '(code: -32000)', // RPC error code -32000 indicates authentication failure @@ -23,14 +14,6 @@ const AUTH_ERROR_PATTERNS = [ /** * Determines if the given error is authentication-related - * - * This function detects various forms of authentication errors, including: - * - Direct error objects - * - String-form error messages - * - Other types of errors converted to strings for pattern matching - * - * @param error - The error object to check, can be an Error instance, string, or other type - * @returns boolean - true if the error is authentication-related, false otherwise */ export const isAuthenticationRequiredError = (error: unknown): boolean => { // Null check to avoid unnecessary processing diff --git a/packages/vscode-ide-companion/src/utils/authNotificationHandler.ts b/packages/vscode-ide-companion/src/utils/authNotificationHandler.ts index 3586e042..362867c2 100644 --- a/packages/vscode-ide-companion/src/utils/authNotificationHandler.ts +++ b/packages/vscode-ide-companion/src/utils/authNotificationHandler.ts @@ -7,12 +7,12 @@ 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; +// Store reference to the current notification +let currentNotification: Thenable | null = null; /** * Handle authentication update notifications by showing a VS Code notification - * with the authentication URI and a copy button. + * with the authentication URI and action buttons. * * @param data - Authentication update notification data containing the auth URI */ @@ -21,30 +21,21 @@ 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 - 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.`, + // Store reference to the current notification + currentNotification = vscode.window.showInformationMessage( + `Qwen Code needs authentication. Click an action below:`, 'Open in Browser', 'Copy Link', + 'Dismiss', ); - // Create a simple disposable object - authNotificationDisposable = { - dispose: () => { - // We can't actually cancel the promise, but we can clear our reference - }, - }; - - notificationPromise.then((selection) => { + currentNotification.then((selection) => { if (selection === 'Open in Browser') { // Open the authentication URI in the default browser vscode.env.openExternal(vscode.Uri.parse(authUri)); + vscode.window.showInformationMessage( + 'Opening authentication page in your browser...', + ); } else if (selection === 'Copy Link') { // Copy the authentication URI to clipboard vscode.env.clipboard.writeText(authUri); @@ -54,6 +45,6 @@ export function handleAuthenticateUpdate( } // Clear the notification reference after user interaction - authNotificationDisposable = null; + currentNotification = null; }); } diff --git a/packages/vscode-ide-companion/src/webview/WebViewProvider.ts b/packages/vscode-ide-companion/src/webview/WebViewProvider.ts index e0b533a0..7e3c97c6 100644 --- a/packages/vscode-ide-companion/src/webview/WebViewProvider.ts +++ b/packages/vscode-ide-companion/src/webview/WebViewProvider.ts @@ -8,12 +8,11 @@ import * as vscode from 'vscode'; import { QwenAgentManager } from '../services/qwenAgentManager.js'; import { ConversationStore } from '../services/conversationStore.js'; import type { AcpPermissionRequest } from '../types/acpTypes.js'; -import { CliDetector } from '../cli/cliDetector.js'; +import { CliManager } from '../cli/cliManager.js'; 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'; @@ -566,7 +565,7 @@ export class WebViewProvider { ); // Check if CLI is installed before attempting to connect - const cliDetection = await CliDetector.detectQwenCli(); + const cliDetection = await CliManager.detectQwenCli(); if (!cliDetection.isInstalled) { console.log( @@ -590,7 +589,7 @@ export class WebViewProvider { console.log('[WebViewProvider] CLI version:', cliDetection.version); // Perform version check with throttled notifications - const versionChecker = CliVersionChecker.getInstance(this.context); + const versionChecker = CliManager.getInstance(this.context); await versionChecker.checkCliVersion(true); // Silent check to avoid popup spam try { @@ -674,7 +673,6 @@ export class WebViewProvider { return vscode.window.withProgress( { location: vscode.ProgressLocation.Notification, - title: 'Logging in to Qwen Code... ', cancellable: false, }, async (progress) => { diff --git a/packages/vscode-ide-companion/src/webview/components/messages/Assistant/AssistantMessage.tsx b/packages/vscode-ide-companion/src/webview/components/messages/Assistant/AssistantMessage.tsx index ed8badcc..84712efa 100644 --- a/packages/vscode-ide-companion/src/webview/components/messages/Assistant/AssistantMessage.tsx +++ b/packages/vscode-ide-companion/src/webview/components/messages/Assistant/AssistantMessage.tsx @@ -75,7 +75,11 @@ export const AssistantMessage: React.FC = ({ whiteSpace: 'normal', }} > - +
From ccc619216431fb2f069a654353e0fa3ee505c8ac Mon Sep 17 00:00:00 2001 From: yiliang114 <1204183885@qq.com> Date: Sat, 13 Dec 2025 21:37:10 +0800 Subject: [PATCH 12/18] chore(vscode-ide-companion): wip --- .../src/cli/cliManager.ts | 498 ------------------ .../src/services/qwenAgentManager.ts | 1 - .../src/services/qwenConnectionHandler.ts | 138 +---- .../src/webview/WebViewProvider.ts | 126 ++--- 4 files changed, 55 insertions(+), 708 deletions(-) delete mode 100644 packages/vscode-ide-companion/src/cli/cliManager.ts diff --git a/packages/vscode-ide-companion/src/cli/cliManager.ts b/packages/vscode-ide-companion/src/cli/cliManager.ts deleted file mode 100644 index a11fe668..00000000 --- a/packages/vscode-ide-companion/src/cli/cliManager.ts +++ /dev/null @@ -1,498 +0,0 @@ -/** - * @license - * Copyright 2025 Qwen Team - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as vscode from 'vscode'; -import { exec } from 'child_process'; -import { promisify } from 'util'; -import semver from 'semver'; -import { CliInstaller } from './cliInstaller.js'; - -const execAsync = promisify(exec); - -export const MIN_CLI_VERSION_FOR_SESSION_METHODS = '0.5.0'; - -export interface CliDetectionResult { - isInstalled: boolean; - cliPath?: string; - version?: string; - error?: string; -} - -export interface CliFeatureFlags { - supportsSessionList: boolean; - supportsSessionLoad: boolean; -} - -export interface CliVersionInfo { - version: string | undefined; - isSupported: boolean; - features: CliFeatureFlags; - detectionResult: CliDetectionResult; -} - -export class CliManager { - private static instance: CliManager; - private lastNotificationTime: number = 0; - private static readonly NOTIFICATION_COOLDOWN_MS = 300000; // 5 minutes cooldown - private context: vscode.ExtensionContext | undefined; - - // Cache mechanisms - private static cachedDetectionResult: CliDetectionResult | null = null; - private static detectionLastCheckTime: number = 0; - private cachedVersionInfo: CliVersionInfo | null = null; - private versionLastCheckTime: number = 0; - private static readonly CACHE_DURATION_MS = 30000; // 30 seconds - - private constructor(context?: vscode.ExtensionContext) { - this.context = context; - } - - /** - * Get singleton instance - */ - static getInstance(context?: vscode.ExtensionContext): CliManager { - if (!CliManager.instance && context) { - CliManager.instance = new CliManager(context); - } - return CliManager.instance; - } - - /** - * Checks if the Qwen Code CLI is installed - * @param forceRefresh - Force a new check, ignoring cache - * @returns Detection result with installation status and details - */ - static async detectQwenCli( - forceRefresh = false, - ): Promise { - const now = Date.now(); - - // Return cached result if available and not expired - if ( - !forceRefresh && - this.cachedDetectionResult && - now - this.detectionLastCheckTime < this.CACHE_DURATION_MS - ) { - console.log('[CliManager] Returning cached detection result'); - return this.cachedDetectionResult; - } - - console.log( - '[CliManager] Starting CLI detection, current PATH:', - process.env.PATH, - ); - - try { - const isWindows = process.platform === 'win32'; - const whichCommand = isWindows ? 'where' : 'which'; - - // Check if qwen command exists - try { - // Use NVM environment for consistent detection - // Fallback chain: default alias -> node alias -> current version - const detectionCommand = - process.platform === 'win32' - ? `${whichCommand} qwen` - : 'source ~/.nvm/nvm.sh 2>/dev/null && (nvm use default 2>/dev/null || nvm use node 2>/dev/null || nvm use 2>/dev/null); which qwen'; - - console.log( - '[CliManager] Detecting CLI with command:', - detectionCommand, - ); - - const { stdout } = await execAsync(detectionCommand, { - timeout: 5000, - shell: isWindows ? undefined : '/bin/bash', - }); - // The output may contain multiple lines, with NVM activation messages - // We want the last line which should be the actual path - const lines = stdout - .trim() - .split('\n') - .filter((line) => line.trim()); - const cliPath = lines[lines.length - 1]; - - console.log('[CliManager] Found CLI at:', cliPath); - - // Try to get version - let version: string | undefined; - try { - // Use NVM environment for version check - // Fallback chain: default alias -> node alias -> current version - // Also ensure we use the correct Node.js version that matches the CLI installation - const versionCommand = - process.platform === 'win32' - ? 'qwen --version' - : 'source ~/.nvm/nvm.sh 2>/dev/null && (nvm use default 2>/dev/null || nvm use node 2>/dev/null || nvm use 2>/dev/null); qwen --version'; - - console.log( - '[CliManager] Getting version with command:', - versionCommand, - ); - - const { stdout: versionOutput } = await execAsync(versionCommand, { - timeout: 5000, - shell: isWindows ? undefined : '/bin/bash', - }); - // The output may contain multiple lines, with NVM activation messages - // We want the last line which should be the actual version - const versionLines = versionOutput - .trim() - .split('\n') - .filter((line) => line.trim()); - version = versionLines[versionLines.length - 1]; - console.log('[CliManager] CLI version:', version); - } catch (versionError) { - console.log('[CliManager] Failed to get CLI version:', versionError); - // Version check failed, but CLI is installed - } - - this.cachedDetectionResult = { - isInstalled: true, - cliPath, - version, - }; - this.detectionLastCheckTime = now; - return this.cachedDetectionResult; - } catch (detectionError) { - console.log('[CliManager] CLI not found, error:', detectionError); - // CLI not found - let error = `Qwen Code CLI not found in PATH. Please install it using: npm install -g @qwen-code/qwen-code@latest`; - - // Provide specific guidance for permission errors - if (detectionError instanceof Error) { - const errorMessage = detectionError.message; - if ( - errorMessage.includes('EACCES') || - errorMessage.includes('Permission denied') - ) { - error += `\n\nThis may be due to permission issues. Possible solutions: - \n1. Reinstall the CLI without sudo: npm install -g @qwen-code/qwen-code@latest - \n2. If previously installed with sudo, fix ownership: sudo chown -R $(whoami) $(npm config get prefix)/lib/node_modules/@qwen-code/qwen-code - \n3. Use nvm for Node.js version management to avoid permission issues - \n4. Check your PATH environment variable includes npm's global bin directory`; - } - } - - this.cachedDetectionResult = { - isInstalled: false, - error, - }; - this.detectionLastCheckTime = now; - return this.cachedDetectionResult; - } - } catch (error) { - console.log('[CliManager] General detection error:', error); - const errorMessage = - error instanceof Error ? error.message : String(error); - - let userFriendlyError = `Failed to detect Qwen Code CLI: ${errorMessage}`; - - // Provide specific guidance for permission errors - if ( - errorMessage.includes('EACCES') || - errorMessage.includes('Permission denied') - ) { - userFriendlyError += `\n\nThis may be due to permission issues. Possible solutions: - \n1. Reinstall the CLI without sudo: npm install -g @qwen-code/qwen-code@latest - \n2. If previously installed with sudo, fix ownership: sudo chown -R $(whoami) $(npm config get prefix)/lib/node_modules/@qwen-code/qwen-code - \n3. Use nvm for Node.js version management to avoid permission issues - \n4. Check your PATH environment variable includes npm's global bin directory`; - } - - this.cachedDetectionResult = { - isInstalled: false, - error: userFriendlyError, - }; - this.detectionLastCheckTime = now; - return this.cachedDetectionResult; - } - } - - /** - * Clears the cached detection result - */ - static clearCache(): void { - this.cachedDetectionResult = null; - this.detectionLastCheckTime = 0; - } - - /** - * Gets installation instructions based on the platform - */ - static getInstallationInstructions(): { - title: string; - steps: string[]; - documentationUrl: string; - } { - return { - title: 'Qwen Code CLI is not installed', - steps: [ - 'Install via npm:', - ' npm install -g @qwen-code/qwen-code@latest', - '', - 'If you are using nvm (automatically handled by the plugin):', - ' The plugin will automatically use your default nvm version', - '', - 'Or install from source:', - ' git clone https://github.com/QwenLM/qwen-code.git', - ' cd qwen-code', - ' npm install', - ' npm install -g .', - '', - 'After installation, reload VS Code or restart the extension.', - ], - documentationUrl: 'https://github.com/QwenLM/qwen-code#installation', - }; - } - - /** - * Check if CLI version meets minimum requirements - * - * @param version - Version string to check - * @param minVersion - Minimum required version - * @returns Whether version meets requirements - */ - private isVersionSupported( - version: string | undefined, - minVersion: string, - ): boolean { - if (!version) { - return false; - } - - // Use semver for robust comparison (handles v-prefix, pre-release, etc.) - const v = semver.valid(version) ?? semver.coerce(version)?.version ?? null; - const min = - semver.valid(minVersion) ?? semver.coerce(minVersion)?.version ?? null; - - if (!v || !min) { - console.warn( - `[CliManager] Invalid semver: version=${version}, min=${minVersion}`, - ); - return false; - } - console.log(`[CliManager] Version ${v} meets requirements: ${min}`); - return semver.gte(v, min); - } - - /** - * Get feature flags based on CLI version - * - * @param version - CLI version string - * @returns Feature flags - */ - private getFeatureFlags(version: string | undefined): CliFeatureFlags { - const isSupportedVersion = this.isVersionSupported( - version, - MIN_CLI_VERSION_FOR_SESSION_METHODS, - ); - - return { - supportsSessionList: isSupportedVersion, - supportsSessionLoad: isSupportedVersion, - }; - } - - /** - * Detect CLI version and features - * - * @param forceRefresh - Force a new check, ignoring cache - * @returns CLI version information - */ - async detectCliVersion(forceRefresh = false): Promise { - const now = Date.now(); - - // Return cached result if available and not expired - if ( - !forceRefresh && - this.cachedVersionInfo && - now - this.versionLastCheckTime < CliManager.CACHE_DURATION_MS - ) { - console.log('[CliManager] Returning cached version info'); - return this.cachedVersionInfo; - } - - console.log('[CliManager] Detecting CLI version...'); - - try { - // Detect CLI installation - const detectionResult = await CliManager.detectQwenCli(forceRefresh); - - const versionInfo: CliVersionInfo = { - version: detectionResult.version, - isSupported: this.isVersionSupported( - detectionResult.version, - MIN_CLI_VERSION_FOR_SESSION_METHODS, - ), - features: this.getFeatureFlags(detectionResult.version), - detectionResult, - }; - - // Cache the result - this.cachedVersionInfo = versionInfo; - this.versionLastCheckTime = now; - - console.log('[CliManager] CLI version detection result:', versionInfo); - - return versionInfo; - } catch (error) { - console.error('[CliManager] Failed to detect CLI version:', error); - - // Return fallback result - const fallbackResult: CliVersionInfo = { - version: undefined, - isSupported: false, - features: { - supportsSessionList: false, - supportsSessionLoad: false, - }, - detectionResult: { - isInstalled: false, - error: error instanceof Error ? error.message : String(error), - }, - }; - - return fallbackResult; - } - } - - /** - * Clear cached version information - */ - clearVersionCache(): void { - this.cachedVersionInfo = null; - this.versionLastCheckTime = 0; - CliManager.clearCache(); - } - - /** - * Check if CLI supports session/list method - * - * @param forceRefresh - Force a new check, ignoring cache - * @returns Whether session/list is supported - */ - async supportsSessionList(forceRefresh = false): Promise { - const versionInfo = await this.detectCliVersion(forceRefresh); - return versionInfo.features.supportsSessionList; - } - - /** - * Check if CLI supports session/load method - * - * @param forceRefresh - Force a new check, ignoring cache - * @returns Whether session/load is supported - */ - async supportsSessionLoad(forceRefresh = false): Promise { - const versionInfo = await this.detectCliVersion(forceRefresh); - return versionInfo.features.supportsSessionLoad; - } - - /** - * 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 CliManager.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`, - ); - this.lastNotificationTime = Date.now(); - } - - return { - isInstalled: false, - error: detectionResult.error, - isSupported: false, - needsUpdate: false, - }; - } - - // Get version information - const versionInfo = await this.detectCliVersion(); - - const currentVersion = detectionResult.version; - const isSupported = versionInfo.isSupported; - - // Check if update is needed (version is too old) - const needsUpdate = currentVersion - ? !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 ${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`, - 'Upgrade Now', - 'View Documentation', - ) - .then(async (selection) => { - if (selection === 'Upgrade Now') { - await CliInstaller.install(); - } else if (selection === 'View Documentation') { - vscode.env.openExternal( - vscode.Uri.parse( - 'https://github.com/QwenLM/qwen-code#installation', - ), - ); - } - }); - this.lastNotificationTime = Date.now(); - } - - return { - isInstalled: true, - version: currentVersion, - isSupported, - needsUpdate, - }; - } catch (error) { - console.error('[CliManager] 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), - isSupported: false, - needsUpdate: false, - }; - } - } - - /** - * Check if notification can be shown based on cooldown period - */ - private canShowNotification(): boolean { - return ( - Date.now() - this.lastNotificationTime > - CliManager.NOTIFICATION_COOLDOWN_MS - ); - } -} diff --git a/packages/vscode-ide-companion/src/services/qwenAgentManager.ts b/packages/vscode-ide-companion/src/services/qwenAgentManager.ts index 9fd92274..2d94a3b3 100644 --- a/packages/vscode-ide-companion/src/services/qwenAgentManager.ts +++ b/packages/vscode-ide-companion/src/services/qwenAgentManager.ts @@ -197,7 +197,6 @@ export class QwenAgentManager { this.currentWorkingDir = workingDir; return this.connectionHandler.connect( this.connection, - this.sessionReader, workingDir, cliEntryPath, options, diff --git a/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts b/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts index 690c28e8..28f2db0b 100644 --- a/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts +++ b/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts @@ -11,9 +11,6 @@ */ import type { AcpConnection } from './acpConnection.js'; -import type { QwenSessionReader } from '../services/qwenSessionReader.js'; -import { authMethod } from '../types/acpTypes.js'; -import { isAuthenticationRequiredError } from '../utils/authErrors.js'; export interface QwenConnectionResult { sessionCreated: boolean; @@ -29,156 +26,31 @@ export class QwenConnectionHandler { * Connect to Qwen service and establish session * * @param connection - ACP connection instance - * @param sessionReader - Session reader instance * @param workingDir - Working directory * @param cliEntryPath - Path to bundled CLI entrypoint (cli.js) */ async connect( connection: AcpConnection, - sessionReader: QwenSessionReader, workingDir: string, cliEntryPath: string, - options?: { - autoAuthenticate?: boolean; - }, ): Promise { const connectId = Date.now(); console.log(`[QwenAgentManager] 🚀 CONNECT() CALLED - ID: ${connectId}`); - const autoAuthenticate = options?.autoAuthenticate ?? true; - let sessionCreated = false; - let requiresAuth = false; + const sessionCreated = false; + const requiresAuth = false; // Build extra CLI arguments (only essential parameters) const extraArgs: string[] = []; await connection.connect(cliEntryPath, workingDir, extraArgs); - // Try to restore existing session or create new session - // Note: Auto-restore on connect is disabled to avoid surprising loads - // when user opens a "New Chat" tab. Restoration is now an explicit action - // (session selector → session/load) or handled by higher-level flows. - const sessionRestored = false; - - // Create new session if unable to restore - if (!sessionRestored) { - console.log( - '[QwenAgentManager] no sessionRestored, Creating new session...', - ); - - try { - console.log( - '[QwenAgentManager] Creating new session (letting CLI handle authentication)...', - ); - await this.newSessionWithRetry( - connection, - workingDir, - 3, - authMethod, - autoAuthenticate, - ); - console.log('[QwenAgentManager] New session created successfully'); - sessionCreated = true; - } catch (sessionError) { - const needsAuth = - autoAuthenticate === false && - isAuthenticationRequiredError(sessionError); - if (needsAuth) { - requiresAuth = true; - console.log( - '[QwenAgentManager] Session creation requires authentication; waiting for user-triggered login.', - ); - } else { - console.log( - `\n⚠️ [SESSION FAILED] newSessionWithRetry threw error\n`, - ); - console.log(`[QwenAgentManager] Error details:`, sessionError); - throw sessionError; - } - } - } else { - sessionCreated = true; - } + // Note: Session creation is now handled by the caller (QwenAgentManager) + // This prevents automatic session creation on every connection which was + // causing unwanted authentication prompts console.log(`\n========================================`); console.log(`[QwenAgentManager] ✅ CONNECT() COMPLETED SUCCESSFULLY`); console.log(`========================================\n`); return { sessionCreated, requiresAuth }; } - - /** - * Create new session (with retry) - * - * @param connection - ACP connection instance - * @param workingDir - Working directory - * @param maxRetries - Maximum number of retries - */ - private async newSessionWithRetry( - connection: AcpConnection, - workingDir: string, - maxRetries: number, - authMethod: string, - autoAuthenticate: boolean, - ): Promise { - for (let attempt = 1; attempt <= maxRetries; attempt++) { - try { - console.log( - `[QwenAgentManager] Creating session (attempt ${attempt}/${maxRetries})...`, - ); - await connection.newSession(workingDir); - console.log('[QwenAgentManager] Session created successfully'); - return; - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error); - console.error( - `[QwenAgentManager] Session creation attempt ${attempt} failed:`, - errorMessage, - ); - - // If Qwen reports that authentication is required, try to - // authenticate on-the-fly once and retry without waiting. - const requiresAuth = isAuthenticationRequiredError(error); - if (requiresAuth) { - if (!autoAuthenticate) { - console.log( - '[QwenAgentManager] Authentication required but auto-authentication is disabled. Propagating error.', - ); - throw error; - } - console.log( - '[QwenAgentManager] Qwen requires authentication. Authenticating and retrying session/new...', - ); - try { - await connection.authenticate(authMethod); - // FIXME: @yiliang114 If there is no delay for a while, immediately executing - // newSession may cause the cli authorization jump to be triggered again - // Add a slight delay to ensure auth state is settled - await new Promise((resolve) => setTimeout(resolve, 300)); - // Retry immediately after successful auth - await connection.newSession(workingDir); - console.log( - '[QwenAgentManager] Session created successfully after auth', - ); - return; - } catch (authErr) { - console.error( - '[QwenAgentManager] Re-authentication failed:', - authErr, - ); - // Fall through to retry logic below - } - } - - if (attempt === maxRetries) { - throw new Error( - `Session creation failed after ${maxRetries} attempts: ${errorMessage}`, - ); - } - - const delay = Math.min(1000 * Math.pow(2, attempt - 1), 5000); - console.log(`[QwenAgentManager] Retrying in ${delay}ms...`); - await new Promise((resolve) => setTimeout(resolve, delay)); - } - } - } } diff --git a/packages/vscode-ide-companion/src/webview/WebViewProvider.ts b/packages/vscode-ide-companion/src/webview/WebViewProvider.ts index 721c20d6..4ab55283 100644 --- a/packages/vscode-ide-companion/src/webview/WebViewProvider.ts +++ b/packages/vscode-ide-companion/src/webview/WebViewProvider.ts @@ -570,94 +570,68 @@ export class WebViewProvider { ).fsPath; try { - console.log('[WebViewProvider] Connecting to bundled agent...'); - console.log('[WebViewProvider] Bundled CLI entry:', bundledCliEntry); + console.log('[WebViewProvider] Connecting to agent...'); - await this.agentManager.connect(workingDir, bundledCliEntry); + // Pass the detected CLI path to ensure we use the correct installation + const connectResult = await this.agentManager.connect( + workingDir, + bundledCliEntry, + options, + ); console.log('[WebViewProvider] Agent connected successfully'); this.agentInitialized = true; - // Load messages from the current Qwen session - await this.loadCurrentSessionMessages(); + // 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; + } - // Notify webview that agent is connected - this.sendMessageToWebView({ - type: 'agentConnected', - data: {}, - }); + if (connectResult.requiresAuth) { + this.sendMessageToWebView({ + type: 'authState', + data: { authenticated: false }, + }); + } + + // Load messages from the current Qwen session + const sessionReady = await this.loadCurrentSessionMessages(options); + + if (sessionReady) { + // Notify webview that agent is connected + this.sendMessageToWebView({ + type: 'agentConnected', + data: {}, + }); + } else { + console.log( + '[WebViewProvider] Session creation deferred until user logs in.', + ); + } } catch (_error) { console.error('[WebViewProvider] Agent connection error:', _error); vscode.window.showWarningMessage( - `Failed to start bundled Qwen Code CLI: ${_error}\nYou can still use the chat UI, but messages won't be sent to AI.`, + `Failed to connect to Qwen CLI: ${_error}\nYou can still use the chat UI, but messages won't be sent to AI.`, ); // Fallback to empty conversation await this.initializeEmptyConversation(); - try { - console.log('[WebViewProvider] Connecting to agent...'); - - // Pass the detected CLI path to ensure we use the correct installation - const connectResult = await this.agentManager.connect( - workingDir, - bundledCliEntry, - options, - ); - 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', - data: { authenticated: false }, - }); - } - - // Load messages from the current Qwen session - const sessionReady = await this.loadCurrentSessionMessages(options); - - if (sessionReady) { - // Notify webview that agent is connected - this.sendMessageToWebView({ - type: 'agentConnected', - data: {}, - }); - } else { - console.log( - '[WebViewProvider] Session creation deferred until user logs in.', - ); - } - } catch (_error) { - console.error('[WebViewProvider] Agent connection error:', _error); - vscode.window.showWarningMessage( - `Failed to connect to Qwen CLI: ${_error}\nYou can still use the chat UI, but messages won't be sent to AI.`, - ); - // Fallback to empty conversation - await this.initializeEmptyConversation(); - - // Notify webview that agent connection failed - this.sendMessageToWebView({ - type: 'agentConnectionError', - data: { - message: - _error instanceof Error ? _error.message : String(_error), - }, - }); - } + // Notify webview that agent connection failed + this.sendMessageToWebView({ + type: 'agentConnectionError', + data: { + message: _error instanceof Error ? _error.message : String(_error), + }, + }); } }; From 44fef9339996078581da4bd23791ade0a78b1b80 Mon Sep 17 00:00:00 2001 From: yiliang114 <1204183885@qq.com> Date: Sat, 13 Dec 2025 21:39:41 +0800 Subject: [PATCH 13/18] chore(vscode-ide-companion): wip --- .../vscode-ide-companion/src/services/qwenAgentManager.ts | 5 ----- packages/vscode-ide-companion/src/webview/WebViewProvider.ts | 1 - 2 files changed, 6 deletions(-) diff --git a/packages/vscode-ide-companion/src/services/qwenAgentManager.ts b/packages/vscode-ide-companion/src/services/qwenAgentManager.ts index 2d94a3b3..86a72756 100644 --- a/packages/vscode-ide-companion/src/services/qwenAgentManager.ts +++ b/packages/vscode-ide-companion/src/services/qwenAgentManager.ts @@ -34,9 +34,6 @@ export type { ChatMessage, PlanEntry, ToolCallUpdateData }; * * Coordinates various modules and provides unified interface */ -interface AgentConnectOptions { - autoAuthenticate?: boolean; -} interface AgentSessionOptions { autoAuthenticate?: boolean; } @@ -192,14 +189,12 @@ export class QwenAgentManager { async connect( workingDir: string, cliEntryPath: string, - options?: AgentConnectOptions, ): Promise { this.currentWorkingDir = workingDir; return this.connectionHandler.connect( this.connection, workingDir, cliEntryPath, - options, ); } diff --git a/packages/vscode-ide-companion/src/webview/WebViewProvider.ts b/packages/vscode-ide-companion/src/webview/WebViewProvider.ts index 4ab55283..87d771bc 100644 --- a/packages/vscode-ide-companion/src/webview/WebViewProvider.ts +++ b/packages/vscode-ide-companion/src/webview/WebViewProvider.ts @@ -576,7 +576,6 @@ export class WebViewProvider { const connectResult = await this.agentManager.connect( workingDir, bundledCliEntry, - options, ); console.log('[WebViewProvider] Agent connected successfully'); this.agentInitialized = true; From 641dd03689dd6d0fe8a9af8595d3280a34229f60 Mon Sep 17 00:00:00 2001 From: yiliang114 <1204183885@qq.com> Date: Sat, 13 Dec 2025 22:03:16 +0800 Subject: [PATCH 14/18] chore(vscode-ide-companion): wip --- packages/vscode-ide-companion/src/services/qwenAgentManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vscode-ide-companion/src/services/qwenAgentManager.ts b/packages/vscode-ide-companion/src/services/qwenAgentManager.ts index 86a72756..c288d39a 100644 --- a/packages/vscode-ide-companion/src/services/qwenAgentManager.ts +++ b/packages/vscode-ide-companion/src/services/qwenAgentManager.ts @@ -1094,7 +1094,7 @@ export class QwenAgentManager { // Let CLI handle authentication - it's the single source of truth await this.connection.authenticate(authMethod); // Add a slight delay to ensure auth state is settled - await new Promise((resolve) => setTimeout(resolve, 300)); + await new Promise((resolve) => setTimeout(resolve, 500)); await this.connection.newSession(workingDir); } catch (reauthErr) { console.error( From 0f2f1faee55a71332d884b01192905d172f34411 Mon Sep 17 00:00:00 2001 From: yiliang114 <1204183885@qq.com> Date: Sat, 13 Dec 2025 22:19:54 +0800 Subject: [PATCH 15/18] chore(vscode-ide-companion): wip --- .../src/services/qwenAgentManager.ts | 49 ++++-- .../src/services/qwenConnectionHandler.ts | 139 +++++++++++++++++- .../src/webview/WebViewProvider.ts | 1 + 3 files changed, 166 insertions(+), 23 deletions(-) diff --git a/packages/vscode-ide-companion/src/services/qwenAgentManager.ts b/packages/vscode-ide-companion/src/services/qwenAgentManager.ts index c288d39a..cce68a53 100644 --- a/packages/vscode-ide-companion/src/services/qwenAgentManager.ts +++ b/packages/vscode-ide-companion/src/services/qwenAgentManager.ts @@ -34,6 +34,9 @@ export type { ChatMessage, PlanEntry, ToolCallUpdateData }; * * Coordinates various modules and provides unified interface */ +interface AgentConnectOptions { + autoAuthenticate?: boolean; +} interface AgentSessionOptions { autoAuthenticate?: boolean; } @@ -189,12 +192,14 @@ export class QwenAgentManager { async connect( workingDir: string, cliEntryPath: string, + options?: AgentConnectOptions, ): Promise { this.currentWorkingDir = workingDir; return this.connectionHandler.connect( this.connection, workingDir, cliEntryPath, + options, ); } @@ -276,9 +281,10 @@ export class QwenAgentManager { '[QwenAgentManager] Getting session list with version-aware strategy', ); - // Prefer ACP method first; fall back to file system if it fails for any reason. try { - console.log('[QwenAgentManager] Attempting to get session list via ACP'); + 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); @@ -288,19 +294,21 @@ export class QwenAgentManager { const res: unknown = response; let items: Array> = []; - if (Array.isArray(res)) { - items = res as Array>; - } else if (res && typeof res === 'object' && 'items' in res) { + // 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:', { - count: items.length, - }); - + console.log( + '[QwenAgentManager] Sessions retrieved via ACP:', + res, + items.length, + ); if (items.length > 0) { const sessions = items.map((item) => ({ id: item.sessionId || item.id, @@ -314,6 +322,11 @@ export class QwenAgentManager { filePath: item.filePath, cwd: item.cwd, })); + + console.log( + '[QwenAgentManager] Sessions retrieved via ACP:', + sessions.length, + ); return sessions; } } catch (error) { @@ -376,6 +389,7 @@ export class QwenAgentManager { }> { const size = params?.size ?? 20; const cursor = params?.cursor; + try { const response = await this.connection.listSessions({ size, @@ -470,7 +484,6 @@ export class QwenAgentManager { */ async getSessionMessages(sessionId: string): Promise { try { - // Prefer reading CLI's JSONL if we can find filePath from session/list try { const list = await this.getSessionList(); const item = list.find( @@ -690,7 +703,9 @@ export class QwenAgentManager { const planText = planEntries .map( (entry: Record, index: number) => - `${index + 1}. ${entry.description || entry.title || 'Unnamed step'}`, + `${index + 1}. ${ + entry.description || entry.title || 'Unnamed step' + }`, ) .join('\n'); msgs.push({ @@ -969,13 +984,15 @@ export class QwenAgentManager { sessionId, ); - // Prefer ACP session/load first; fall back to file system on failure. try { - console.log('[QwenAgentManager] Attempting to load session via ACP'); + console.log( + '[QwenAgentManager] Attempting to load session via ACP method', + ); await this.loadSessionViaAcp(sessionId); console.log('[QwenAgentManager] Session loaded successfully via ACP'); - // After loading via ACP, we still need to get messages from file system. - // In future, we might get them directly from the ACP response. + + // After loading via ACP, we still need to get messages from file system + // In future, we might get them directly from the ACP response } catch (error) { console.warn( '[QwenAgentManager] ACP session load failed, falling back to file system method:', @@ -1094,7 +1111,7 @@ export class QwenAgentManager { // Let CLI handle authentication - it's the single source of truth await this.connection.authenticate(authMethod); // Add a slight delay to ensure auth state is settled - await new Promise((resolve) => setTimeout(resolve, 500)); + await new Promise((resolve) => setTimeout(resolve, 300)); await this.connection.newSession(workingDir); } catch (reauthErr) { console.error( diff --git a/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts b/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts index 28f2db0b..13bbd6aa 100644 --- a/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts +++ b/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts @@ -11,6 +11,8 @@ */ import type { AcpConnection } from './acpConnection.js'; +import { isAuthenticationRequiredError } from '../utils/authErrors.js'; +import { authMethod } from '../types/acpTypes.js'; export interface QwenConnectionResult { sessionCreated: boolean; @@ -27,30 +29,153 @@ export class QwenConnectionHandler { * * @param connection - ACP connection instance * @param workingDir - Working directory - * @param cliEntryPath - Path to bundled CLI entrypoint (cli.js) + * @param cliPath - CLI path (optional, if provided will override the path in configuration) */ async connect( connection: AcpConnection, workingDir: string, cliEntryPath: string, + options?: { + autoAuthenticate?: boolean; + }, ): Promise { const connectId = Date.now(); console.log(`[QwenAgentManager] 🚀 CONNECT() CALLED - ID: ${connectId}`); - const sessionCreated = false; - const requiresAuth = false; + const autoAuthenticate = options?.autoAuthenticate ?? true; + let sessionCreated = false; + let requiresAuth = false; // Build extra CLI arguments (only essential parameters) const extraArgs: string[] = []; - await connection.connect(cliEntryPath, workingDir, extraArgs); + await connection.connect(cliEntryPath!, workingDir, extraArgs); - // Note: Session creation is now handled by the caller (QwenAgentManager) - // This prevents automatic session creation on every connection which was - // causing unwanted authentication prompts + // Try to restore existing session or create new session + // Note: Auto-restore on connect is disabled to avoid surprising loads + // when user opens a "New Chat" tab. Restoration is now an explicit action + // (session selector → session/load) or handled by higher-level flows. + const sessionRestored = false; + + // Create new session if unable to restore + if (!sessionRestored) { + console.log( + '[QwenAgentManager] no sessionRestored, Creating new session...', + ); + + try { + console.log( + '[QwenAgentManager] Creating new session (letting CLI handle authentication)...', + ); + await this.newSessionWithRetry( + connection, + workingDir, + 3, + authMethod, + autoAuthenticate, + ); + console.log('[QwenAgentManager] New session created successfully'); + sessionCreated = true; + } catch (sessionError) { + const needsAuth = + autoAuthenticate === false && + isAuthenticationRequiredError(sessionError); + if (needsAuth) { + requiresAuth = true; + console.log( + '[QwenAgentManager] Session creation requires authentication; waiting for user-triggered login.', + ); + } else { + console.log( + `\n⚠️ [SESSION FAILED] newSessionWithRetry threw error\n`, + ); + console.log(`[QwenAgentManager] Error details:`, sessionError); + throw sessionError; + } + } + } else { + sessionCreated = true; + } console.log(`\n========================================`); console.log(`[QwenAgentManager] ✅ CONNECT() COMPLETED SUCCESSFULLY`); console.log(`========================================\n`); return { sessionCreated, requiresAuth }; } + + /** + * Create new session (with retry) + * + * @param connection - ACP connection instance + * @param workingDir - Working directory + * @param maxRetries - Maximum number of retries + */ + private async newSessionWithRetry( + connection: AcpConnection, + workingDir: string, + maxRetries: number, + authMethod: string, + autoAuthenticate: boolean, + ): Promise { + for (let attempt = 1; attempt <= maxRetries; attempt++) { + try { + console.log( + `[QwenAgentManager] Creating session (attempt ${attempt}/${maxRetries})...`, + ); + await connection.newSession(workingDir); + console.log('[QwenAgentManager] Session created successfully'); + return; + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : String(error); + console.error( + `[QwenAgentManager] Session creation attempt ${attempt} failed:`, + errorMessage, + ); + + // If Qwen reports that authentication is required, try to + // authenticate on-the-fly once and retry without waiting. + const requiresAuth = isAuthenticationRequiredError(error); + if (requiresAuth) { + if (!autoAuthenticate) { + console.log( + '[QwenAgentManager] Authentication required but auto-authentication is disabled. Propagating error.', + ); + throw error; + } + console.log( + '[QwenAgentManager] Qwen requires authentication. Authenticating and retrying session/new...', + ); + try { + await connection.authenticate(authMethod); + // FIXME: @yiliang114 If there is no delay for a while, immediately executing + // newSession may cause the cli authorization jump to be triggered again + // Add a slight delay to ensure auth state is settled + await new Promise((resolve) => setTimeout(resolve, 300)); + // Retry immediately after successful auth + await connection.newSession(workingDir); + console.log( + '[QwenAgentManager] Session created successfully after auth', + ); + return; + } catch (authErr) { + console.error( + '[QwenAgentManager] Re-authentication failed:', + authErr, + ); + // Fall through to retry logic below + } + } + + if (attempt === maxRetries) { + throw new Error( + `Session creation failed after ${maxRetries} attempts: ${errorMessage}`, + ); + } + + const delay = Math.min(1000 * Math.pow(2, attempt - 1), 5000); + console.log(`[QwenAgentManager] Retrying in ${delay}ms...`); + await new Promise((resolve) => setTimeout(resolve, delay)); + } + } + } } diff --git a/packages/vscode-ide-companion/src/webview/WebViewProvider.ts b/packages/vscode-ide-companion/src/webview/WebViewProvider.ts index 87d771bc..4ab55283 100644 --- a/packages/vscode-ide-companion/src/webview/WebViewProvider.ts +++ b/packages/vscode-ide-companion/src/webview/WebViewProvider.ts @@ -576,6 +576,7 @@ export class WebViewProvider { const connectResult = await this.agentManager.connect( workingDir, bundledCliEntry, + options, ); console.log('[WebViewProvider] Agent connected successfully'); this.agentInitialized = true; From 3138aa1fe3dd1325bdb19162d1af38ba0c3678e3 Mon Sep 17 00:00:00 2001 From: yiliang114 <1204183885@qq.com> Date: Sat, 13 Dec 2025 22:30:10 +0800 Subject: [PATCH 16/18] chore(vscode-ide-companion): wip --- .../vscode-ide-companion/src/services/acpSessionManager.ts | 2 +- packages/vscode-ide-companion/src/services/qwenAgentManager.ts | 3 +++ .../vscode-ide-companion/src/services/qwenConnectionHandler.ts | 3 +++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/vscode-ide-companion/src/services/acpSessionManager.ts b/packages/vscode-ide-companion/src/services/acpSessionManager.ts index d313cba7..cfa299bf 100644 --- a/packages/vscode-ide-companion/src/services/acpSessionManager.ts +++ b/packages/vscode-ide-companion/src/services/acpSessionManager.ts @@ -169,7 +169,7 @@ export class AcpSessionManager { pendingRequests, nextRequestId, ); - console.log('[ACP] Authenticate successful'); + console.log('[ACP] Authenticate successful', response); return response; } diff --git a/packages/vscode-ide-companion/src/services/qwenAgentManager.ts b/packages/vscode-ide-companion/src/services/qwenAgentManager.ts index cce68a53..e60ee3a2 100644 --- a/packages/vscode-ide-companion/src/services/qwenAgentManager.ts +++ b/packages/vscode-ide-companion/src/services/qwenAgentManager.ts @@ -1110,6 +1110,9 @@ export class QwenAgentManager { try { // Let CLI handle authentication - it's the single source of truth await this.connection.authenticate(authMethod); + console.log( + '[QwenAgentManager] createNewSession Authentication successful. Retrying session/new...', + ); // Add a slight delay to ensure auth state is settled await new Promise((resolve) => setTimeout(resolve, 300)); await this.connection.newSession(workingDir); diff --git a/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts b/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts index 13bbd6aa..c66ee23c 100644 --- a/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts +++ b/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts @@ -151,6 +151,9 @@ export class QwenConnectionHandler { // newSession may cause the cli authorization jump to be triggered again // Add a slight delay to ensure auth state is settled await new Promise((resolve) => setTimeout(resolve, 300)); + console.log( + '[QwenAgentManager] newSessionWithRetry Authentication successful', + ); // Retry immediately after successful auth await connection.newSession(workingDir); console.log( From 0e6ebe85e400de900a69ead999083cd64e57369a Mon Sep 17 00:00:00 2001 From: tanzhenxin Date: Sat, 13 Dec 2025 23:04:00 +0800 Subject: [PATCH 17/18] test --- packages/cli/src/acp-integration/acpAgent.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/acp-integration/acpAgent.ts b/packages/cli/src/acp-integration/acpAgent.ts index 53cb8b1c..91ce53cb 100644 --- a/packages/cli/src/acp-integration/acpAgent.ts +++ b/packages/cli/src/acp-integration/acpAgent.ts @@ -292,7 +292,7 @@ class GeminiAgent { private async ensureAuthenticated(config: Config): Promise { const selectedType = this.settings.merged.security?.auth?.selectedType; if (!selectedType) { - throw acp.RequestError.authRequired(); + throw acp.RequestError.authRequired('No Selected Type'); } try { @@ -300,7 +300,9 @@ class GeminiAgent { await config.refreshAuth(selectedType, true); } catch (e) { console.error(`Authentication failed: ${e}`); - throw acp.RequestError.authRequired(); + throw acp.RequestError.authRequired( + 'Authentication failed: ' + (e as Error).message, + ); } } From f7ef720e3b0d881ce4d093525af548f1099c4e04 Mon Sep 17 00:00:00 2001 From: tanzhenxin Date: Sat, 13 Dec 2025 23:18:49 +0800 Subject: [PATCH 18/18] test --- packages/core/src/qwen/qwenOAuth2.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/packages/core/src/qwen/qwenOAuth2.ts b/packages/core/src/qwen/qwenOAuth2.ts index 3cb94a82..77c5345a 100644 --- a/packages/core/src/qwen/qwenOAuth2.ts +++ b/packages/core/src/qwen/qwenOAuth2.ts @@ -677,6 +677,19 @@ async function authWithQwenDeviceFlow( // Cache the new tokens await cacheQwenCredentials(credentials); + // IMPORTANT: + // SharedTokenManager maintains an in-memory cache and throttles file checks. + // If we only write the creds file here, a subsequent `getQwenOAuthClient()` + // call in the same process (within the throttle window) may not re-read the + // updated file and could incorrectly re-trigger device auth. + // Clearing the cache forces the next call to reload from disk. + try { + SharedTokenManager.getInstance().clearCache(); + } catch { + // In unit tests we sometimes mock SharedTokenManager.getInstance() with a + // minimal stub; cache invalidation is best-effort and should not break auth. + } + // Emit auth progress success event qwenOAuth2Events.emit( QwenOAuth2Event.AuthProgress, @@ -880,6 +893,14 @@ export async function clearQwenCredentials(): Promise { } // Log other errors but don't throw - clearing credentials should be non-critical console.warn('Warning: Failed to clear cached Qwen credentials:', error); + } finally { + // Also clear SharedTokenManager in-memory cache to prevent stale credentials + // from being reused within the same process after the file is removed. + try { + SharedTokenManager.getInstance().clearCache(); + } catch { + // Best-effort; don't fail credential clearing if SharedTokenManager is mocked. + } } }