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] 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(); -};