From 7adb9ed7ff91e5bf688f905cf50d23a6b21464b5 Mon Sep 17 00:00:00 2001 From: yiliang114 <1204183885@qq.com> Date: Mon, 8 Dec 2025 23:12:04 +0800 Subject: [PATCH] refactor(vscode-ide-companion): introduce ApprovalMode enum and improve mode handling - Create new approvalModeTypes.ts with ApprovalMode enum and helper functions - Replace hardcoded string literals with ApprovalModeValue type - Consolidate mode mapping logic using new helper functions --- .../src/services/acpConnection.ts | 5 +- .../src/services/acpSessionManager.ts | 5 +- .../src/services/qwenAgentManager.ts | 18 ++--- .../src/types/acpTypes.ts | 18 ++--- .../src/types/approvalModeTypes.ts | 79 +++++++++++++++++++ .../src/types/chatTypes.ts | 13 +-- .../vscode-ide-companion/src/webview/App.tsx | 35 ++++---- .../src/webview/WebViewProvider.ts | 7 +- .../webview/components/layout/InputForm.tsx | 59 +++++++------- .../components/messages/MessageContent.tsx | 5 -- .../components/messages/ThinkingMessage.tsx | 7 -- .../src/webview/hooks/useWebViewMessages.ts | 39 +++------ 12 files changed, 154 insertions(+), 136 deletions(-) create mode 100644 packages/vscode-ide-companion/src/types/approvalModeTypes.ts diff --git a/packages/vscode-ide-companion/src/services/acpConnection.ts b/packages/vscode-ide-companion/src/services/acpConnection.ts index 71bd025c..5486e14d 100644 --- a/packages/vscode-ide-companion/src/services/acpConnection.ts +++ b/packages/vscode-ide-companion/src/services/acpConnection.ts @@ -10,6 +10,7 @@ import type { AcpPermissionRequest, AcpResponse, AcpSessionUpdate, + ApprovalModeValue, } from '../types/acpTypes.js'; import type { ChildProcess, SpawnOptions } from 'child_process'; import { spawn } from 'child_process'; @@ -380,9 +381,7 @@ export class AcpConnection { /** * Set approval mode */ - async setMode( - modeId: 'plan' | 'default' | 'auto-edit' | 'yolo', - ): Promise { + async setMode(modeId: ApprovalModeValue): Promise { return this.sessionManager.setMode( modeId, this.child, diff --git a/packages/vscode-ide-companion/src/services/acpSessionManager.ts b/packages/vscode-ide-companion/src/services/acpSessionManager.ts index 1d01cc2a..78429aa3 100644 --- a/packages/vscode-ide-companion/src/services/acpSessionManager.ts +++ b/packages/vscode-ide-companion/src/services/acpSessionManager.ts @@ -15,6 +15,7 @@ import type { AcpRequest, AcpNotification, AcpResponse, + ApprovalModeValue, } from '../types/acpTypes.js'; import { AGENT_METHODS } from '../constants/acpSchema.js'; import type { PendingRequest } from '../types/connectionTypes.js'; @@ -341,10 +342,10 @@ export class AcpSessionManager { /** * Set approval mode for current session (ACP session/set_mode) * - * @param modeId - 'plan' | 'default' | 'auto-edit' | 'yolo' + * @param modeId - Approval mode value */ async setMode( - modeId: 'plan' | 'default' | 'auto-edit' | 'yolo', + modeId: ApprovalModeValue, child: ChildProcess | null, pendingRequests: Map>, nextRequestId: { value: number }, diff --git a/packages/vscode-ide-companion/src/services/qwenAgentManager.ts b/packages/vscode-ide-companion/src/services/qwenAgentManager.ts index 0bd5a4d9..1bc55f96 100644 --- a/packages/vscode-ide-companion/src/services/qwenAgentManager.ts +++ b/packages/vscode-ide-companion/src/services/qwenAgentManager.ts @@ -7,6 +7,7 @@ import { AcpConnection } from './acpConnection.js'; import type { AcpSessionUpdate, AcpPermissionRequest, + ApprovalModeValue, } from '../types/acpTypes.js'; import { QwenSessionReader, type QwenSession } from './qwenSessionReader.js'; import { QwenSessionManager } from './qwenSessionManager.js'; @@ -138,21 +139,12 @@ export class QwenAgentManager { } /** - * Set approval mode from UI (maps UI edit mode -> ACP mode id) + * Set approval mode from UI */ async setApprovalModeFromUi( - uiMode: 'ask' | 'auto' | 'plan' | 'yolo', - ): Promise<'plan' | 'default' | 'auto-edit' | 'yolo'> { - const map: Record< - 'ask' | 'auto' | 'plan' | 'yolo', - 'plan' | 'default' | 'auto-edit' | 'yolo' - > = { - plan: 'plan', - ask: 'default', - auto: 'auto-edit', - yolo: 'yolo', - } as const; - const modeId = map[uiMode]; + mode: ApprovalModeValue, + ): Promise { + const modeId = mode; try { const res = await this.connection.setMode(modeId); // Optimistically notify UI using response diff --git a/packages/vscode-ide-companion/src/types/acpTypes.ts b/packages/vscode-ide-companion/src/types/acpTypes.ts index d4557826..43c74cd3 100644 --- a/packages/vscode-ide-companion/src/types/acpTypes.ts +++ b/packages/vscode-ide-companion/src/types/acpTypes.ts @@ -7,8 +7,6 @@ export const JSONRPC_VERSION = '2.0' as const; export const authMethod = 'qwen-oauth'; -export type AcpBackend = 'qwen'; - export interface AcpRequest { jsonrpc: typeof JSONRPC_VERSION; id: number; @@ -36,7 +34,6 @@ export interface AcpNotification { params?: unknown; } -// Base interface for all session updates export interface BaseSessionUpdate { sessionId: string; } @@ -50,7 +47,6 @@ export interface ContentBlock { uri?: string; } -// User message chunk update export interface UserMessageChunkUpdate extends BaseSessionUpdate { update: { sessionUpdate: 'user_message_chunk'; @@ -58,7 +54,6 @@ export interface UserMessageChunkUpdate extends BaseSessionUpdate { }; } -// Agent message chunk update export interface AgentMessageChunkUpdate extends BaseSessionUpdate { update: { sessionUpdate: 'agent_message_chunk'; @@ -66,7 +61,6 @@ export interface AgentMessageChunkUpdate extends BaseSessionUpdate { }; } -// Agent thought chunk update export interface AgentThoughtChunkUpdate extends BaseSessionUpdate { update: { sessionUpdate: 'agent_thought_chunk'; @@ -74,7 +68,6 @@ export interface AgentThoughtChunkUpdate extends BaseSessionUpdate { }; } -// Tool call update export interface ToolCallUpdate extends BaseSessionUpdate { update: { sessionUpdate: 'tool_call'; @@ -109,7 +102,6 @@ export interface ToolCallUpdate extends BaseSessionUpdate { }; } -// Tool call status update export interface ToolCallStatusUpdate extends BaseSessionUpdate { update: { sessionUpdate: 'tool_call_update'; @@ -135,7 +127,6 @@ export interface ToolCallStatusUpdate extends BaseSessionUpdate { }; } -// Plan update export interface PlanUpdate extends BaseSessionUpdate { update: { sessionUpdate: 'plan'; @@ -147,9 +138,15 @@ export interface PlanUpdate extends BaseSessionUpdate { }; } -// Approval/Mode values as defined by ACP schema export type ApprovalModeValue = 'plan' | 'default' | 'auto-edit' | 'yolo'; +export { + ApprovalMode, + APPROVAL_MODE_MAP, + APPROVAL_MODE_INFO, + getApprovalModeInfoFromString, +} from './approvalModeTypes.js'; + // Current mode update (sent by agent when mode changes) export interface CurrentModeUpdate extends BaseSessionUpdate { update: { @@ -158,7 +155,6 @@ export interface CurrentModeUpdate extends BaseSessionUpdate { }; } -// Union type for all session updates export type AcpSessionUpdate = | UserMessageChunkUpdate | AgentMessageChunkUpdate diff --git a/packages/vscode-ide-companion/src/types/approvalModeTypes.ts b/packages/vscode-ide-companion/src/types/approvalModeTypes.ts new file mode 100644 index 00000000..ac9b22e5 --- /dev/null +++ b/packages/vscode-ide-companion/src/types/approvalModeTypes.ts @@ -0,0 +1,79 @@ +/** + * @license + * Copyright 2025 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Enum for approval modes with UI-friendly labels + * Represents the different approval modes available in the ACP protocol + * with their corresponding user-facing display names + */ +export enum ApprovalMode { + PLAN = 'plan', + DEFAULT = 'default', + AUTO_EDIT = 'auto-edit', + YOLO = 'yolo', +} + +/** + * Mapping from string values to enum values for runtime conversion + */ +export const APPROVAL_MODE_MAP: Record = { + plan: ApprovalMode.PLAN, + default: ApprovalMode.DEFAULT, + 'auto-edit': ApprovalMode.AUTO_EDIT, + yolo: ApprovalMode.YOLO, +}; + +/** + * UI display information for each approval mode + */ +export const APPROVAL_MODE_INFO: Record< + ApprovalMode, + { + label: string; + title: string; + iconType?: 'edit' | 'auto' | 'plan' | 'yolo'; + } +> = { + [ApprovalMode.PLAN]: { + label: 'Plan mode', + title: 'Qwen will plan before executing. Click to switch modes.', + iconType: 'plan', + }, + [ApprovalMode.DEFAULT]: { + label: 'Ask before edits', + title: 'Qwen will ask before each edit. Click to switch modes.', + iconType: 'edit', + }, + [ApprovalMode.AUTO_EDIT]: { + label: 'Edit automatically', + title: 'Qwen will edit files automatically. Click to switch modes.', + iconType: 'auto', + }, + [ApprovalMode.YOLO]: { + label: 'YOLO', + title: 'Automatically approve all tools. Click to switch modes.', + iconType: 'yolo', + }, +}; + +/** + * Get UI display information for an approval mode from string value + */ +export function getApprovalModeInfoFromString(mode: string): { + label: string; + title: string; + iconType?: 'edit' | 'auto' | 'plan' | 'yolo'; +} { + const enumValue = APPROVAL_MODE_MAP[mode]; + if (enumValue !== undefined) { + return APPROVAL_MODE_INFO[enumValue]; + } + return { + label: 'Unknown mode', + title: 'Unknown edit mode', + iconType: undefined, + }; +} diff --git a/packages/vscode-ide-companion/src/types/chatTypes.ts b/packages/vscode-ide-companion/src/types/chatTypes.ts index 3b9c6153..ae530081 100644 --- a/packages/vscode-ide-companion/src/types/chatTypes.ts +++ b/packages/vscode-ide-companion/src/types/chatTypes.ts @@ -3,7 +3,7 @@ * Copyright 2025 Qwen Team * SPDX-License-Identifier: Apache-2.0 */ -import type { AcpPermissionRequest } from './acpTypes.js'; +import type { AcpPermissionRequest, ApprovalModeValue } from './acpTypes.js'; export interface ChatMessage { role: 'user' | 'assistant'; @@ -66,15 +66,15 @@ export interface QwenAgentCallbacks { onEndTurn?: () => void; /** Callback for receiving mode information after ACP initialization */ onModeInfo?: (info: { - currentModeId?: 'plan' | 'default' | 'auto-edit' | 'yolo'; + currentModeId?: ApprovalModeValue; availableModes?: Array<{ - id: 'plan' | 'default' | 'auto-edit' | 'yolo'; + id: ApprovalModeValue; name: string; description: string; }>; }) => void; /** Callback for receiving notifications when the mode changes */ - onModeChanged?: (modeId: 'plan' | 'default' | 'auto-edit' | 'yolo') => void; + onModeChanged?: (modeId: ApprovalModeValue) => void; } /** @@ -105,8 +105,3 @@ export interface ToolCallUpdate { }>; timestamp?: number; // Add timestamp field for message ordering } - -/** - * Edit mode type - */ -export type EditMode = 'ask' | 'auto' | 'plan' | 'yolo'; diff --git a/packages/vscode-ide-companion/src/webview/App.tsx b/packages/vscode-ide-companion/src/webview/App.tsx index 61d35867..42b62876 100644 --- a/packages/vscode-ide-companion/src/webview/App.tsx +++ b/packages/vscode-ide-companion/src/webview/App.tsx @@ -42,7 +42,8 @@ import { import { InputForm } from './components/layout/InputForm.js'; import { SessionSelector } from './components/layout/SessionSelector.js'; import { FileIcon, UserIcon } from './components/icons/index.js'; -import type { EditMode } from '../types/chatTypes.js'; +import { ApprovalMode } from '../types/acpTypes.js'; +import type { ApprovalModeValue } from '../types/acpTypes.js'; import type { PlanEntry } from '../types/chatTypes.js'; export const App: React.FC = () => { @@ -77,7 +78,9 @@ export const App: React.FC = () => { null, ) as React.RefObject; - const [editMode, setEditMode] = useState('ask'); + const [editMode, setEditMode] = useState( + ApprovalMode.DEFAULT, + ); const [thinkingEnabled, setThinkingEnabled] = useState(false); const [isComposing, setIsComposing] = useState(false); // When true, do NOT auto-attach the active editor file/selection to message context @@ -461,30 +464,22 @@ export const App: React.FC = () => { }); }, [vscode]); - // Handle toggle edit mode (Ask -> Auto -> Plan -> YOLO -> Ask) + // Handle toggle edit mode (Default -> Auto-edit -> Plan -> YOLO -> Default) const handleToggleEditMode = useCallback(() => { setEditMode((prev) => { - const next: EditMode = - prev === 'ask' - ? 'auto' - : prev === 'auto' - ? 'plan' - : prev === 'plan' - ? 'yolo' - : 'ask'; + const next: ApprovalModeValue = + prev === ApprovalMode.DEFAULT + ? ApprovalMode.AUTO_EDIT + : prev === ApprovalMode.AUTO_EDIT + ? ApprovalMode.PLAN + : prev === ApprovalMode.PLAN + ? ApprovalMode.YOLO + : ApprovalMode.DEFAULT; // Notify extension to set approval mode via ACP try { - const toAcp = - next === 'plan' - ? 'plan' - : next === 'auto' - ? 'auto-edit' - : next === 'yolo' - ? 'yolo' - : 'default'; vscode.postMessage({ type: 'setApprovalMode', - data: { modeId: toAcp }, + data: { modeId: next }, }); } catch { /* no-op */ diff --git a/packages/vscode-ide-companion/src/webview/WebViewProvider.ts b/packages/vscode-ide-companion/src/webview/WebViewProvider.ts index 64a317fd..8ca503ac 100644 --- a/packages/vscode-ide-companion/src/webview/WebViewProvider.ts +++ b/packages/vscode-ide-companion/src/webview/WebViewProvider.ts @@ -15,7 +15,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 { authMethod } from '../types/acpTypes.js'; +import { authMethod, type ApprovalModeValue } from '../types/acpTypes.js'; export class WebViewProvider { private panelManager: PanelManager; @@ -31,8 +31,7 @@ export class WebViewProvider { private pendingPermissionRequest: AcpPermissionRequest | null = null; private pendingPermissionResolve: ((optionId: string) => void) | null = null; // Track current ACP mode id to influence permission/diff behavior - private currentModeId: 'plan' | 'default' | 'auto-edit' | 'yolo' | null = - null; + private currentModeId: ApprovalModeValue | null = null; constructor( context: vscode.ExtensionContext, @@ -885,7 +884,7 @@ export class WebViewProvider { } /** Get current ACP mode id (if known). */ - getCurrentModeId(): 'plan' | 'default' | 'auto-edit' | 'yolo' | null { + getCurrentModeId(): ApprovalModeValue | null { return this.currentModeId; } 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 cd33e55b..fe86ea99 100644 --- a/packages/vscode-ide-companion/src/webview/components/layout/InputForm.tsx +++ b/packages/vscode-ide-companion/src/webview/components/layout/InputForm.tsx @@ -19,7 +19,8 @@ import { } from '../icons/index.js'; import { CompletionMenu } from '../layout/CompletionMenu.js'; import type { CompletionItem } from '../../../types/completionItemTypes.js'; -import type { EditMode } from '../../../types/chatTypes.js'; +import { getApprovalModeInfoFromString } from '../../../types/acpTypes.js'; +import type { ApprovalModeValue } from '../../../types/acpTypes.js'; interface InputFormProps { inputText: string; @@ -29,7 +30,7 @@ interface InputFormProps { isStreaming: boolean; isWaitingForResponse: boolean; isComposing: boolean; - editMode: EditMode; + editMode: ApprovalModeValue; thinkingEnabled: boolean; activeFileName: string | null; activeSelection: { startLine: number; endLine: number } | null; @@ -53,41 +54,35 @@ interface InputFormProps { onCompletionClose?: () => void; } -// Get edit mode display info -const getEditModeInfo = (editMode: EditMode) => { - switch (editMode) { - case 'ask': - return { - text: 'Ask before edits', - title: 'Qwen will ask before each edit. Click to switch modes.', - icon: , - }; +// Get edit mode display info using helper function +const getEditModeInfo = (editMode: ApprovalModeValue) => { + const info = getApprovalModeInfoFromString(editMode); + + // Map icon types to actual icons + let icon = null; + switch (info.iconType) { + case 'edit': + icon = ; + break; case 'auto': - return { - text: 'Edit automatically', - title: 'Qwen will edit files automatically. Click to switch modes.', - icon: , - }; + icon = ; + break; case 'plan': - return { - text: 'Plan mode', - title: 'Qwen will plan before executing. Click to switch modes.', - icon: , - }; + icon = ; + break; case 'yolo': - return { - text: 'YOLO', - title: 'Automatically approve all tools. Click to switch modes.', - // Reuse Auto icon for simplicity; can swap to a distinct icon later. - icon: , - }; + icon = ; + break; default: - return { - text: 'Unknown mode', - title: 'Unknown edit mode', - icon: null, - }; + icon = null; + break; } + + return { + text: info.label, + title: info.title, + icon, + }; }; export const InputForm: React.FC = ({ diff --git a/packages/vscode-ide-companion/src/webview/components/messages/MessageContent.tsx b/packages/vscode-ide-companion/src/webview/components/messages/MessageContent.tsx index 52ce0fc2..c3437e6e 100644 --- a/packages/vscode-ide-companion/src/webview/components/messages/MessageContent.tsx +++ b/packages/vscode-ide-companion/src/webview/components/messages/MessageContent.tsx @@ -2,8 +2,6 @@ * @license * Copyright 2025 Qwen Team * SPDX-License-Identifier: Apache-2.0 - * - * MessageContent component - renders message with code highlighting and clickable file paths */ import type React from 'react'; @@ -14,9 +12,6 @@ interface MessageContentProps { onFileClick?: (filePath: string) => void; } -/** - * MessageContent component - renders message content with markdown support - */ export const MessageContent: React.FC = ({ content, onFileClick, diff --git a/packages/vscode-ide-companion/src/webview/components/messages/ThinkingMessage.tsx b/packages/vscode-ide-companion/src/webview/components/messages/ThinkingMessage.tsx index ab366b72..1f92e1f4 100644 --- a/packages/vscode-ide-companion/src/webview/components/messages/ThinkingMessage.tsx +++ b/packages/vscode-ide-companion/src/webview/components/messages/ThinkingMessage.tsx @@ -37,12 +37,5 @@ export const ThinkingMessage: React.FC = ({ - {/* Timestamp - temporarily hidden */} - {/*
- {new Date(timestamp).toLocaleTimeString()} -
*/} ); diff --git a/packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.ts b/packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.ts index 1d58f2ed..85ec4391 100644 --- a/packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.ts +++ b/packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.ts @@ -11,7 +11,8 @@ import type { PermissionOption, ToolCall as PermissionToolCall, } from '../components/PermissionDrawer/PermissionRequest.js'; -import type { ToolCallUpdate, EditMode } from '../../types/chatTypes.js'; +import type { ToolCallUpdate } from '../../types/chatTypes.js'; +import type { ApprovalModeValue } from '../../types/acpTypes.js'; import type { PlanEntry } from '../../types/chatTypes.js'; interface UseWebViewMessagesProps { @@ -107,7 +108,7 @@ interface UseWebViewMessagesProps { inputFieldRef: React.RefObject; setInputText: (text: string) => void; // Edit mode setter (maps ACP modes to UI modes) - setEditMode?: (mode: EditMode) => void; + setEditMode?: (mode: ApprovalModeValue) => void; } /** @@ -196,20 +197,9 @@ export const useWebViewMessages = ({ case 'modeInfo': { // Initialize UI mode from ACP initialize try { - const current = (message.data?.currentModeId || 'default') as - | 'plan' - | 'default' - | 'auto-edit' - | 'yolo'; - setEditMode?.( - (current === 'plan' - ? 'plan' - : current === 'auto-edit' - ? 'auto' - : current === 'yolo' - ? 'yolo' - : 'ask') as EditMode, - ); + const current = (message.data?.currentModeId || + 'default') as ApprovalModeValue; + setEditMode?.(current); } catch (_error) { // best effort } @@ -218,20 +208,9 @@ export const useWebViewMessages = ({ case 'modeChanged': { try { - const modeId = (message.data?.modeId || 'default') as - | 'plan' - | 'default' - | 'auto-edit' - | 'yolo'; - setEditMode?.( - (modeId === 'plan' - ? 'plan' - : modeId === 'auto-edit' - ? 'auto' - : modeId === 'yolo' - ? 'yolo' - : 'ask') as EditMode, - ); + const modeId = (message.data?.modeId || + 'default') as ApprovalModeValue; + setEditMode?.(modeId); } catch (_error) { // Ignore error when setting mode }