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.
This commit is contained in:
yiliang114
2025-12-13 00:01:05 +08:00
parent 8b29dd130e
commit f5306339f6
13 changed files with 103 additions and 26 deletions

View File

@@ -10,8 +10,8 @@ import type {
AcpPermissionRequest, AcpPermissionRequest,
AcpResponse, AcpResponse,
AcpSessionUpdate, AcpSessionUpdate,
ApprovalModeValue,
} from '../types/acpTypes.js'; } from '../types/acpTypes.js';
import type { ApprovalModeValue } from '../types/approvalModeValueTypes.js';
import type { ChildProcess, SpawnOptions } from 'child_process'; import type { ChildProcess, SpawnOptions } from 'child_process';
import { spawn } from 'child_process'; import { spawn } from 'child_process';
import type { import type {

View File

@@ -14,8 +14,8 @@ import type {
AcpRequest, AcpRequest,
AcpNotification, AcpNotification,
AcpResponse, AcpResponse,
ApprovalModeValue,
} from '../types/acpTypes.js'; } from '../types/acpTypes.js';
import type { ApprovalModeValue } from '../types/approvalModeValueTypes.js';
import { AGENT_METHODS } from '../constants/acpSchema.js'; import { AGENT_METHODS } from '../constants/acpSchema.js';
import type { PendingRequest } from '../types/connectionTypes.js'; import type { PendingRequest } from '../types/connectionTypes.js';
import type { ChildProcess } from 'child_process'; import type { ChildProcess } from 'child_process';

View File

@@ -7,8 +7,8 @@ import { AcpConnection } from './acpConnection.js';
import type { import type {
AcpSessionUpdate, AcpSessionUpdate,
AcpPermissionRequest, AcpPermissionRequest,
ApprovalModeValue,
} from '../types/acpTypes.js'; } from '../types/acpTypes.js';
import type { ApprovalModeValue } from '../types/approvalModeValueTypes.js';
import { QwenSessionReader, type QwenSession } from './qwenSessionReader.js'; import { QwenSessionReader, type QwenSession } from './qwenSessionReader.js';
import { QwenSessionManager } from './qwenSessionManager.js'; import { QwenSessionManager } from './qwenSessionManager.js';
import type { import type {

View File

@@ -10,7 +10,8 @@
* Handles session updates from ACP and dispatches them to appropriate callbacks * 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'; import type { QwenAgentCallbacks } from '../types/chatTypes.js';
/** /**

View File

@@ -3,6 +3,7 @@
* Copyright 2025 Qwen Team * Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
import type { ApprovalModeValue } from './approvalModeValueTypes.js';
export const JSONRPC_VERSION = '2.0' as const; export const JSONRPC_VERSION = '2.0' as const;
export const authMethod = 'qwen-oauth'; export const authMethod = 'qwen-oauth';
@@ -138,8 +139,6 @@ export interface PlanUpdate extends BaseSessionUpdate {
}; };
} }
export type ApprovalModeValue = 'plan' | 'default' | 'auto-edit' | 'yolo';
export { export {
ApprovalMode, ApprovalMode,
APPROVAL_MODE_MAP, APPROVAL_MODE_MAP,

View File

@@ -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';

View File

@@ -3,7 +3,8 @@
* Copyright 2025 Qwen Team * Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0 * 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 { export interface ChatMessage {
role: 'user' | 'assistant'; role: 'user' | 'assistant';

View File

@@ -43,7 +43,7 @@ import { InputForm } from './components/layout/InputForm.js';
import { SessionSelector } from './components/layout/SessionSelector.js'; import { SessionSelector } from './components/layout/SessionSelector.js';
import { FileIcon, UserIcon } from './components/icons/index.js'; import { FileIcon, UserIcon } from './components/icons/index.js';
import { ApprovalMode, NEXT_APPROVAL_MODE } from '../types/acpTypes.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'; import type { PlanEntry } from '../types/chatTypes.js';
export const App: React.FC = () => { export const App: React.FC = () => {
@@ -90,9 +90,13 @@ export const App: React.FC = () => {
const getCompletionItems = React.useCallback( const getCompletionItems = React.useCallback(
async (trigger: '@' | '/', query: string): Promise<CompletionItem[]> => { async (trigger: '@' | '/', query: string): Promise<CompletionItem[]> => {
if (trigger === '@') { if (trigger === '@') {
if (!fileContext.hasRequestedFiles) { console.log('[App] getCompletionItems @ called', {
fileContext.requestWorkspaceFiles(); query,
} requested: fileContext.hasRequestedFiles,
workspaceFiles: fileContext.workspaceFiles.length,
});
// 始终根据当前 query 触发请求,让 hook 判断是否需要真正请求
fileContext.requestWorkspaceFiles(query);
const fileIcon = <FileIcon />; const fileIcon = <FileIcon />;
const allItems: CompletionItem[] = fileContext.workspaceFiles.map( const allItems: CompletionItem[] = fileContext.workspaceFiles.map(
@@ -109,7 +113,6 @@ export const App: React.FC = () => {
); );
if (query && query.length >= 1) { if (query && query.length >= 1) {
fileContext.requestWorkspaceFiles(query);
const lowerQuery = query.toLowerCase(); const lowerQuery = query.toLowerCase();
return allItems.filter( return allItems.filter(
(item) => (item) =>
@@ -154,17 +157,39 @@ export const App: React.FC = () => {
const completion = useCompletionTrigger(inputFieldRef, getCompletionItems); 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 // 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 // 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. // changes on every render which would retrigger this effect and can cause a refresh loop.
useEffect(() => { 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 // Only refresh items; do not change other completion state to avoid re-renders loops
completion.refreshCompletion(); completion.refreshCompletion();
} }
// Only re-run when the actual data source changes, not on every render // Only re-run when the actual data source changes, not on every render
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [fileContext.workspaceFiles, completion.isOpen, completion.triggerChar]); }, [
workspaceFilesSignature,
completion.isOpen,
completion.triggerChar,
completion.query,
]);
// Message submission // Message submission
const { handleSubmit: submitMessage } = useMessageSubmit({ const { handleSubmit: submitMessage } = useMessageSubmit({

View File

@@ -14,7 +14,7 @@ import { MessageHandler } from '../webview/MessageHandler.js';
import { WebViewContent } from '../webview/WebViewContent.js'; import { WebViewContent } from '../webview/WebViewContent.js';
import { CliInstaller } from '../cli/cliInstaller.js'; import { CliInstaller } from '../cli/cliInstaller.js';
import { getFileName } from './utils/webviewUtils.js'; import { getFileName } from './utils/webviewUtils.js';
import { type ApprovalModeValue } from '../types/acpTypes.js'; import { type ApprovalModeValue } from '../types/approvalModeValueTypes.js';
export class WebViewProvider { export class WebViewProvider {
private panelManager: PanelManager; private panelManager: PanelManager;

View File

@@ -20,7 +20,7 @@ import {
import { CompletionMenu } from '../layout/CompletionMenu.js'; import { CompletionMenu } from '../layout/CompletionMenu.js';
import type { CompletionItem } from '../../../types/completionItemTypes.js'; import type { CompletionItem } from '../../../types/completionItemTypes.js';
import { getApprovalModeInfoFromString } from '../../../types/acpTypes.js'; import { getApprovalModeInfoFromString } from '../../../types/acpTypes.js';
import type { ApprovalModeValue } from '../../../types/acpTypes.js'; import type { ApprovalModeValue } from '../../../types/approvalModeValueTypes.js';
interface InputFormProps { interface InputFormProps {
inputText: string; inputText: string;

View File

@@ -7,6 +7,7 @@
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import { BaseMessageHandler } from './BaseMessageHandler.js'; import { BaseMessageHandler } from './BaseMessageHandler.js';
import type { ChatMessage } from '../../services/qwenAgentManager.js'; import type { ChatMessage } from '../../services/qwenAgentManager.js';
import type { ApprovalModeValue } from '../../types/approvalModeValueTypes.js';
/** /**
* Session message handler * Session message handler
@@ -29,6 +30,8 @@ export class SessionMessageHandler extends BaseMessageHandler {
'cancelStreaming', 'cancelStreaming',
// UI action: open a new chat tab (new WebviewPanel) // UI action: open a new chat tab (new WebviewPanel)
'openNewChatTab', 'openNewChatTab',
// Settings-related messages
'setApprovalMode',
].includes(messageType); ].includes(messageType);
} }
@@ -112,6 +115,14 @@ export class SessionMessageHandler extends BaseMessageHandler {
await this.handleCancelStreaming(); await this.handleCancelStreaming();
break; break;
case 'setApprovalMode':
await this.handleSetApprovalMode(
message.data as {
modeId?: ApprovalModeValue;
},
);
break;
default: default:
console.warn( console.warn(
'[SessionMessageHandler] Unknown message type:', '[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<void> {
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}` },
});
}
}
} }

View File

@@ -34,6 +34,9 @@ export const useFileContext = (vscode: VSCodeAPI) => {
// Whether workspace files have been requested // Whether workspace files have been requested
const hasRequestedFilesRef = useRef(false); const hasRequestedFilesRef = useRef(false);
// Last non-empty query to decide when to refetch full list
const lastQueryRef = useRef<string | undefined>(undefined);
// Search debounce timer // Search debounce timer
const searchTimerRef = useRef<NodeJS.Timeout | null>(null); const searchTimerRef = useRef<NodeJS.Timeout | null>(null);
@@ -42,12 +45,10 @@ export const useFileContext = (vscode: VSCodeAPI) => {
*/ */
const requestWorkspaceFiles = useCallback( const requestWorkspaceFiles = useCallback(
(query?: string) => { (query?: string) => {
if (!hasRequestedFilesRef.current && !query) { const normalizedQuery = query?.trim();
hasRequestedFilesRef.current = true;
}
// If there's a query, clear previous timer and set up debounce // 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) { if (searchTimerRef.current) {
clearTimeout(searchTimerRef.current); clearTimeout(searchTimerRef.current);
} }
@@ -55,14 +56,23 @@ export const useFileContext = (vscode: VSCodeAPI) => {
searchTimerRef.current = setTimeout(() => { searchTimerRef.current = setTimeout(() => {
vscode.postMessage({ vscode.postMessage({
type: 'getWorkspaceFiles', type: 'getWorkspaceFiles',
data: { query }, data: { query: normalizedQuery },
}); });
}, 300); }, 300);
lastQueryRef.current = normalizedQuery;
} else { } else {
vscode.postMessage({ // For empty query, request once initially and whenever we are returning from a search
type: 'getWorkspaceFiles', const shouldRequestFullList =
data: query ? { query } : {}, !hasRequestedFilesRef.current || lastQueryRef.current !== undefined;
});
if (shouldRequestFullList) {
lastQueryRef.current = undefined;
hasRequestedFilesRef.current = true;
vscode.postMessage({
type: 'getWorkspaceFiles',
data: {},
});
}
} }
}, },
[vscode], [vscode],

View File

@@ -12,7 +12,7 @@ import type {
ToolCall as PermissionToolCall, ToolCall as PermissionToolCall,
} from '../components/PermissionDrawer/PermissionRequest.js'; } from '../components/PermissionDrawer/PermissionRequest.js';
import type { ToolCallUpdate } from '../../types/chatTypes.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'; import type { PlanEntry } from '../../types/chatTypes.js';
interface UseWebViewMessagesProps { interface UseWebViewMessagesProps {