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 {