mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
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:
@@ -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 {
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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';
|
||||||
@@ -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';
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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}` },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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],
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user