From 65796e27995fad5f9479a1ff14bf4615742c8387 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=93=E8=89=AF?= <1204183885@qq.com> Date: Sat, 13 Dec 2025 17:37:45 +0800 Subject: [PATCH] Fix/vscode ide companion completion menu content (#1243) * 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. * 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. * 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/acpConnection.ts | 2 +- .../src/services/acpSessionManager.ts | 2 +- .../src/services/qwenAgentManager.ts | 94 +-------- .../src/services/qwenConnectionHandler.ts | 11 +- .../src/services/qwenSessionManager.ts | 126 +---------- .../src/services/qwenSessionReader.ts | 196 +++++++++++++++++- .../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/MessageHandler.ts | 7 - .../src/webview/WebViewProvider.ts | 2 +- .../components/layout/CompletionMenu.tsx | 3 +- .../webview/components/layout/InputForm.tsx | 13 +- .../components/layout/SessionSelector.tsx | 6 +- .../webview/handlers/FileMessageHandler.ts | 1 - .../src/webview/handlers/MessageRouter.ts | 16 -- .../webview/handlers/SessionMessageHandler.ts | 141 +++---------- .../handlers/SettingsMessageHandler.ts | 101 --------- .../src/webview/hooks/file/useFileContext.ts | 28 ++- .../src/webview/hooks/useCompletionTrigger.ts | 45 +++- .../src/webview/hooks/useWebViewMessages.ts | 2 +- .../src/webview/utils/sessionGrouping.ts | 35 ++++ .../src/webview/utils/timeUtils.ts | 40 ---- 25 files changed, 400 insertions(+), 530 deletions(-) create mode 100644 packages/vscode-ide-companion/src/types/approvalModeValueTypes.ts 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/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..2475e309 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 { @@ -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/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/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/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/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/InputForm.tsx b/packages/vscode-ide-companion/src/webview/components/layout/InputForm.tsx index fe86ea99..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, @@ -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; @@ -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 */}