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
This commit is contained in:
yiliang114
2025-12-08 23:12:04 +08:00
parent f146f062cb
commit 7adb9ed7ff
12 changed files with 154 additions and 136 deletions

View File

@@ -10,6 +10,7 @@ import type {
AcpPermissionRequest, AcpPermissionRequest,
AcpResponse, AcpResponse,
AcpSessionUpdate, AcpSessionUpdate,
ApprovalModeValue,
} from '../types/acpTypes.js'; } from '../types/acpTypes.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';
@@ -380,9 +381,7 @@ export class AcpConnection {
/** /**
* Set approval mode * Set approval mode
*/ */
async setMode( async setMode(modeId: ApprovalModeValue): Promise<AcpResponse> {
modeId: 'plan' | 'default' | 'auto-edit' | 'yolo',
): Promise<AcpResponse> {
return this.sessionManager.setMode( return this.sessionManager.setMode(
modeId, modeId,
this.child, this.child,

View File

@@ -15,6 +15,7 @@ import type {
AcpRequest, AcpRequest,
AcpNotification, AcpNotification,
AcpResponse, AcpResponse,
ApprovalModeValue,
} from '../types/acpTypes.js'; } from '../types/acpTypes.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';
@@ -341,10 +342,10 @@ export class AcpSessionManager {
/** /**
* Set approval mode for current session (ACP session/set_mode) * Set approval mode for current session (ACP session/set_mode)
* *
* @param modeId - 'plan' | 'default' | 'auto-edit' | 'yolo' * @param modeId - Approval mode value
*/ */
async setMode( async setMode(
modeId: 'plan' | 'default' | 'auto-edit' | 'yolo', modeId: ApprovalModeValue,
child: ChildProcess | null, child: ChildProcess | null,
pendingRequests: Map<number, PendingRequest<unknown>>, pendingRequests: Map<number, PendingRequest<unknown>>,
nextRequestId: { value: number }, nextRequestId: { value: number },

View File

@@ -7,6 +7,7 @@ import { AcpConnection } from './acpConnection.js';
import type { import type {
AcpSessionUpdate, AcpSessionUpdate,
AcpPermissionRequest, AcpPermissionRequest,
ApprovalModeValue,
} from '../types/acpTypes.js'; } from '../types/acpTypes.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';
@@ -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( async setApprovalModeFromUi(
uiMode: 'ask' | 'auto' | 'plan' | 'yolo', mode: ApprovalModeValue,
): Promise<'plan' | 'default' | 'auto-edit' | 'yolo'> { ): Promise<ApprovalModeValue> {
const map: Record< const modeId = mode;
'ask' | 'auto' | 'plan' | 'yolo',
'plan' | 'default' | 'auto-edit' | 'yolo'
> = {
plan: 'plan',
ask: 'default',
auto: 'auto-edit',
yolo: 'yolo',
} as const;
const modeId = map[uiMode];
try { try {
const res = await this.connection.setMode(modeId); const res = await this.connection.setMode(modeId);
// Optimistically notify UI using response // Optimistically notify UI using response

View File

@@ -7,8 +7,6 @@
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';
export type AcpBackend = 'qwen';
export interface AcpRequest { export interface AcpRequest {
jsonrpc: typeof JSONRPC_VERSION; jsonrpc: typeof JSONRPC_VERSION;
id: number; id: number;
@@ -36,7 +34,6 @@ export interface AcpNotification {
params?: unknown; params?: unknown;
} }
// Base interface for all session updates
export interface BaseSessionUpdate { export interface BaseSessionUpdate {
sessionId: string; sessionId: string;
} }
@@ -50,7 +47,6 @@ export interface ContentBlock {
uri?: string; uri?: string;
} }
// User message chunk update
export interface UserMessageChunkUpdate extends BaseSessionUpdate { export interface UserMessageChunkUpdate extends BaseSessionUpdate {
update: { update: {
sessionUpdate: 'user_message_chunk'; sessionUpdate: 'user_message_chunk';
@@ -58,7 +54,6 @@ export interface UserMessageChunkUpdate extends BaseSessionUpdate {
}; };
} }
// Agent message chunk update
export interface AgentMessageChunkUpdate extends BaseSessionUpdate { export interface AgentMessageChunkUpdate extends BaseSessionUpdate {
update: { update: {
sessionUpdate: 'agent_message_chunk'; sessionUpdate: 'agent_message_chunk';
@@ -66,7 +61,6 @@ export interface AgentMessageChunkUpdate extends BaseSessionUpdate {
}; };
} }
// Agent thought chunk update
export interface AgentThoughtChunkUpdate extends BaseSessionUpdate { export interface AgentThoughtChunkUpdate extends BaseSessionUpdate {
update: { update: {
sessionUpdate: 'agent_thought_chunk'; sessionUpdate: 'agent_thought_chunk';
@@ -74,7 +68,6 @@ export interface AgentThoughtChunkUpdate extends BaseSessionUpdate {
}; };
} }
// Tool call update
export interface ToolCallUpdate extends BaseSessionUpdate { export interface ToolCallUpdate extends BaseSessionUpdate {
update: { update: {
sessionUpdate: 'tool_call'; sessionUpdate: 'tool_call';
@@ -109,7 +102,6 @@ export interface ToolCallUpdate extends BaseSessionUpdate {
}; };
} }
// Tool call status update
export interface ToolCallStatusUpdate extends BaseSessionUpdate { export interface ToolCallStatusUpdate extends BaseSessionUpdate {
update: { update: {
sessionUpdate: 'tool_call_update'; sessionUpdate: 'tool_call_update';
@@ -135,7 +127,6 @@ export interface ToolCallStatusUpdate extends BaseSessionUpdate {
}; };
} }
// Plan update
export interface PlanUpdate extends BaseSessionUpdate { export interface PlanUpdate extends BaseSessionUpdate {
update: { update: {
sessionUpdate: 'plan'; 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 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) // Current mode update (sent by agent when mode changes)
export interface CurrentModeUpdate extends BaseSessionUpdate { export interface CurrentModeUpdate extends BaseSessionUpdate {
update: { update: {
@@ -158,7 +155,6 @@ export interface CurrentModeUpdate extends BaseSessionUpdate {
}; };
} }
// Union type for all session updates
export type AcpSessionUpdate = export type AcpSessionUpdate =
| UserMessageChunkUpdate | UserMessageChunkUpdate
| AgentMessageChunkUpdate | AgentMessageChunkUpdate

View File

@@ -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<string, ApprovalMode> = {
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,
};
}

View File

@@ -3,7 +3,7 @@
* Copyright 2025 Qwen Team * Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
import type { AcpPermissionRequest } from './acpTypes.js'; import type { AcpPermissionRequest, ApprovalModeValue } from './acpTypes.js';
export interface ChatMessage { export interface ChatMessage {
role: 'user' | 'assistant'; role: 'user' | 'assistant';
@@ -66,15 +66,15 @@ export interface QwenAgentCallbacks {
onEndTurn?: () => void; onEndTurn?: () => void;
/** Callback for receiving mode information after ACP initialization */ /** Callback for receiving mode information after ACP initialization */
onModeInfo?: (info: { onModeInfo?: (info: {
currentModeId?: 'plan' | 'default' | 'auto-edit' | 'yolo'; currentModeId?: ApprovalModeValue;
availableModes?: Array<{ availableModes?: Array<{
id: 'plan' | 'default' | 'auto-edit' | 'yolo'; id: ApprovalModeValue;
name: string; name: string;
description: string; description: string;
}>; }>;
}) => void; }) => void;
/** Callback for receiving notifications when the mode changes */ /** 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 timestamp?: number; // Add timestamp field for message ordering
} }
/**
* Edit mode type
*/
export type EditMode = 'ask' | 'auto' | 'plan' | 'yolo';

View File

@@ -42,7 +42,8 @@ import {
import { InputForm } from './components/layout/InputForm.js'; 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 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'; import type { PlanEntry } from '../types/chatTypes.js';
export const App: React.FC = () => { export const App: React.FC = () => {
@@ -77,7 +78,9 @@ export const App: React.FC = () => {
null, null,
) as React.RefObject<HTMLDivElement>; ) as React.RefObject<HTMLDivElement>;
const [editMode, setEditMode] = useState<EditMode>('ask'); const [editMode, setEditMode] = useState<ApprovalModeValue>(
ApprovalMode.DEFAULT,
);
const [thinkingEnabled, setThinkingEnabled] = useState(false); const [thinkingEnabled, setThinkingEnabled] = useState(false);
const [isComposing, setIsComposing] = useState(false); const [isComposing, setIsComposing] = useState(false);
// When true, do NOT auto-attach the active editor file/selection to message context // When true, do NOT auto-attach the active editor file/selection to message context
@@ -461,30 +464,22 @@ export const App: React.FC = () => {
}); });
}, [vscode]); }, [vscode]);
// Handle toggle edit mode (Ask -> Auto -> Plan -> YOLO -> Ask) // Handle toggle edit mode (Default -> Auto-edit -> Plan -> YOLO -> Default)
const handleToggleEditMode = useCallback(() => { const handleToggleEditMode = useCallback(() => {
setEditMode((prev) => { setEditMode((prev) => {
const next: EditMode = const next: ApprovalModeValue =
prev === 'ask' prev === ApprovalMode.DEFAULT
? 'auto' ? ApprovalMode.AUTO_EDIT
: prev === 'auto' : prev === ApprovalMode.AUTO_EDIT
? 'plan' ? ApprovalMode.PLAN
: prev === 'plan' : prev === ApprovalMode.PLAN
? 'yolo' ? ApprovalMode.YOLO
: 'ask'; : ApprovalMode.DEFAULT;
// Notify extension to set approval mode via ACP // Notify extension to set approval mode via ACP
try { try {
const toAcp =
next === 'plan'
? 'plan'
: next === 'auto'
? 'auto-edit'
: next === 'yolo'
? 'yolo'
: 'default';
vscode.postMessage({ vscode.postMessage({
type: 'setApprovalMode', type: 'setApprovalMode',
data: { modeId: toAcp }, data: { modeId: next },
}); });
} catch { } catch {
/* no-op */ /* no-op */

View File

@@ -15,7 +15,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 { authMethod } from '../types/acpTypes.js'; import { authMethod, type ApprovalModeValue } from '../types/acpTypes.js';
export class WebViewProvider { export class WebViewProvider {
private panelManager: PanelManager; private panelManager: PanelManager;
@@ -31,8 +31,7 @@ export class WebViewProvider {
private pendingPermissionRequest: AcpPermissionRequest | null = null; private pendingPermissionRequest: AcpPermissionRequest | null = null;
private pendingPermissionResolve: ((optionId: string) => void) | null = null; private pendingPermissionResolve: ((optionId: string) => void) | null = null;
// Track current ACP mode id to influence permission/diff behavior // Track current ACP mode id to influence permission/diff behavior
private currentModeId: 'plan' | 'default' | 'auto-edit' | 'yolo' | null = private currentModeId: ApprovalModeValue | null = null;
null;
constructor( constructor(
context: vscode.ExtensionContext, context: vscode.ExtensionContext,
@@ -885,7 +884,7 @@ export class WebViewProvider {
} }
/** Get current ACP mode id (if known). */ /** Get current ACP mode id (if known). */
getCurrentModeId(): 'plan' | 'default' | 'auto-edit' | 'yolo' | null { getCurrentModeId(): ApprovalModeValue | null {
return this.currentModeId; return this.currentModeId;
} }

View File

@@ -19,7 +19,8 @@ import {
} from '../icons/index.js'; } from '../icons/index.js';
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 type { EditMode } from '../../../types/chatTypes.js'; import { getApprovalModeInfoFromString } from '../../../types/acpTypes.js';
import type { ApprovalModeValue } from '../../../types/acpTypes.js';
interface InputFormProps { interface InputFormProps {
inputText: string; inputText: string;
@@ -29,7 +30,7 @@ interface InputFormProps {
isStreaming: boolean; isStreaming: boolean;
isWaitingForResponse: boolean; isWaitingForResponse: boolean;
isComposing: boolean; isComposing: boolean;
editMode: EditMode; editMode: ApprovalModeValue;
thinkingEnabled: boolean; thinkingEnabled: boolean;
activeFileName: string | null; activeFileName: string | null;
activeSelection: { startLine: number; endLine: number } | null; activeSelection: { startLine: number; endLine: number } | null;
@@ -53,41 +54,35 @@ interface InputFormProps {
onCompletionClose?: () => void; onCompletionClose?: () => void;
} }
// Get edit mode display info // Get edit mode display info using helper function
const getEditModeInfo = (editMode: EditMode) => { const getEditModeInfo = (editMode: ApprovalModeValue) => {
switch (editMode) { const info = getApprovalModeInfoFromString(editMode);
case 'ask':
return { // Map icon types to actual icons
text: 'Ask before edits', let icon = null;
title: 'Qwen will ask before each edit. Click to switch modes.', switch (info.iconType) {
icon: <EditPencilIcon />, case 'edit':
}; icon = <EditPencilIcon />;
break;
case 'auto': case 'auto':
return { icon = <AutoEditIcon />;
text: 'Edit automatically', break;
title: 'Qwen will edit files automatically. Click to switch modes.',
icon: <AutoEditIcon />,
};
case 'plan': case 'plan':
return { icon = <PlanModeIcon />;
text: 'Plan mode', break;
title: 'Qwen will plan before executing. Click to switch modes.',
icon: <PlanModeIcon />,
};
case 'yolo': case 'yolo':
return { icon = <AutoEditIcon />;
text: 'YOLO', break;
title: 'Automatically approve all tools. Click to switch modes.',
// Reuse Auto icon for simplicity; can swap to a distinct icon later.
icon: <AutoEditIcon />,
};
default: default:
return { icon = null;
text: 'Unknown mode', break;
title: 'Unknown edit mode',
icon: null,
};
} }
return {
text: info.label,
title: info.title,
icon,
};
}; };
export const InputForm: React.FC<InputFormProps> = ({ export const InputForm: React.FC<InputFormProps> = ({

View File

@@ -2,8 +2,6 @@
* @license * @license
* Copyright 2025 Qwen Team * Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*
* MessageContent component - renders message with code highlighting and clickable file paths
*/ */
import type React from 'react'; import type React from 'react';
@@ -14,9 +12,6 @@ interface MessageContentProps {
onFileClick?: (filePath: string) => void; onFileClick?: (filePath: string) => void;
} }
/**
* MessageContent component - renders message content with markdown support
*/
export const MessageContent: React.FC<MessageContentProps> = ({ export const MessageContent: React.FC<MessageContentProps> = ({
content, content,
onFileClick, onFileClick,

View File

@@ -37,12 +37,5 @@ export const ThinkingMessage: React.FC<ThinkingMessageProps> = ({
</span> </span>
<MessageContent content={content} onFileClick={onFileClick} /> <MessageContent content={content} onFileClick={onFileClick} />
</div> </div>
{/* Timestamp - temporarily hidden */}
{/* <div
className="text-xs opacity-60"
style={{ color: 'var(--app-secondary-foreground)' }}
>
{new Date(timestamp).toLocaleTimeString()}
</div> */}
</div> </div>
); );

View File

@@ -11,7 +11,8 @@ import type {
PermissionOption, PermissionOption,
ToolCall as PermissionToolCall, ToolCall as PermissionToolCall,
} from '../components/PermissionDrawer/PermissionRequest.js'; } 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'; import type { PlanEntry } from '../../types/chatTypes.js';
interface UseWebViewMessagesProps { interface UseWebViewMessagesProps {
@@ -107,7 +108,7 @@ interface UseWebViewMessagesProps {
inputFieldRef: React.RefObject<HTMLDivElement>; inputFieldRef: React.RefObject<HTMLDivElement>;
setInputText: (text: string) => void; setInputText: (text: string) => void;
// Edit mode setter (maps ACP modes to UI modes) // 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': { case 'modeInfo': {
// Initialize UI mode from ACP initialize // Initialize UI mode from ACP initialize
try { try {
const current = (message.data?.currentModeId || 'default') as const current = (message.data?.currentModeId ||
| 'plan' 'default') as ApprovalModeValue;
| 'default' setEditMode?.(current);
| 'auto-edit'
| 'yolo';
setEditMode?.(
(current === 'plan'
? 'plan'
: current === 'auto-edit'
? 'auto'
: current === 'yolo'
? 'yolo'
: 'ask') as EditMode,
);
} catch (_error) { } catch (_error) {
// best effort // best effort
} }
@@ -218,20 +208,9 @@ export const useWebViewMessages = ({
case 'modeChanged': { case 'modeChanged': {
try { try {
const modeId = (message.data?.modeId || 'default') as const modeId = (message.data?.modeId ||
| 'plan' 'default') as ApprovalModeValue;
| 'default' setEditMode?.(modeId);
| 'auto-edit'
| 'yolo';
setEditMode?.(
(modeId === 'plan'
? 'plan'
: modeId === 'auto-edit'
? 'auto'
: modeId === 'yolo'
? 'yolo'
: 'ask') as EditMode,
);
} catch (_error) { } catch (_error) {
// Ignore error when setting mode // Ignore error when setting mode
} }