fix(vscode-ide-companion): resolve ESLint errors and improve code quality

- Fix unused variable issues by removing unused variables and renaming caught errors to match ESLint rules
- Fix TypeScript type mismatches in mode handling
- Add missing curly braces to if statements to comply with ESLint rules
- Resolve missing dependency warnings in React hooks
- Clean up empty catch blocks by adding appropriate comments
- Remove unused _lastEditorState variables that were declared but never read

These changes ensure the codebase passes ESLint checks and follows best practices for code quality.
This commit is contained in:
yiliang114
2025-12-07 16:57:40 +08:00
parent 67eee14ca9
commit 51b4de0c23
18 changed files with 836 additions and 176 deletions

View File

@@ -63,10 +63,6 @@
{
"command": "qwen-code.login",
"title": "Qwen Code: Login"
},
{
"command": "qwen-code.clearAuthCache",
"title": "Qwen Code: Clear Authentication Cache"
}
],
"configuration": {

View File

@@ -44,6 +44,8 @@ export class AcpConnection {
optionId: string;
}> = () => Promise.resolve({ optionId: 'allow' });
onEndTurn: () => void = () => {};
// Called after successful initialize() with the initialize result
onInitialized: (init: unknown) => void = () => {};
constructor() {
this.messageHandler = new AcpMessageHandler();
@@ -213,6 +215,11 @@ export class AcpConnection {
);
console.log('[ACP] Initialization response:', res);
try {
this.onInitialized(res);
} catch (err) {
console.warn('[ACP] onInitialized callback error:', err);
}
}
/**
@@ -377,6 +384,20 @@ export class AcpConnection {
);
}
/**
* Set approval mode
*/
async setMode(
modeId: 'plan' | 'default' | 'auto-edit' | 'yolo',
): Promise<AcpResponse> {
return this.sessionManager.setMode(
modeId,
this.child,
this.pendingRequests,
this.nextRequestId,
);
}
/**
* Disconnect
*/

View File

@@ -334,6 +334,32 @@ export class AcpSessionManager {
}
}
/**
* Set approval mode for current session (ACP session/set_mode)
*
* @param modeId - 'plan' | 'default' | 'auto-edit' | 'yolo'
*/
async setMode(
modeId: 'plan' | 'default' | 'auto-edit' | 'yolo',
child: ChildProcess | null,
pendingRequests: Map<number, PendingRequest<unknown>>,
nextRequestId: { value: number },
): Promise<AcpResponse> {
if (!this.sessionId) {
throw new Error('No active ACP session');
}
console.log('[ACP] Sending session/set_mode:', modeId);
const res = await this.sendRequest<AcpResponse>(
AGENT_METHODS.session_set_mode,
{ sessionId: this.sessionId, modeId },
child,
pendingRequests,
nextRequestId,
);
console.log('[ACP] set_mode response:', res);
return res;
}
/**
* Switch to specified session
*

View File

@@ -80,6 +80,31 @@ export class QwenAgentManager {
console.warn('[QwenAgentManager] onEndTurn callback error:', err);
}
};
// Initialize callback to surface available modes and current mode to UI
this.connection.onInitialized = (init: unknown) => {
try {
const obj = (init || {}) as Record<string, unknown>;
const modes = obj['modes'] as
| {
currentModeId?: 'plan' | 'default' | 'auto-edit' | 'yolo';
availableModes?: Array<{
id: 'plan' | 'default' | 'auto-edit' | 'yolo';
name: string;
description: string;
}>;
}
| undefined;
if (modes && this.callbacks.onModeInfo) {
this.callbacks.onModeInfo({
currentModeId: modes.currentModeId,
availableModes: modes.availableModes,
});
}
} catch (err) {
console.warn('[QwenAgentManager] onInitialized parse error:', err);
}
};
}
/**
@@ -115,6 +140,41 @@ export class QwenAgentManager {
await this.connection.sendPrompt(message);
}
/**
* Set approval mode from UI (maps UI edit mode -> ACP mode id)
*/
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];
try {
const res = await this.connection.setMode(modeId);
// Optimistically notify UI using response
const result = (res?.result || {}) as { modeId?: string };
const confirmed =
(result.modeId as
| 'plan'
| 'default'
| 'auto-edit'
| 'yolo'
| undefined) || modeId;
this.callbacks.onModeChanged?.(confirmed);
return confirmed;
} catch (err) {
console.error('[QwenAgentManager] Failed to set mode:', err);
throw err;
}
}
/**
* Validate if current session is still active
* This is a lightweight check to verify session validity
@@ -182,17 +242,14 @@ export class QwenAgentManager {
const res: unknown = response;
let items: Array<Record<string, unknown>> = [];
if (
typeof response === 'object' &&
response !== null &&
'items' in response
) {
// Type guard to safely access items property
const responseObject: Record<string, unknown> = response;
if ('items' in responseObject) {
const itemsValue = responseObject.items;
items = Array.isArray(itemsValue) ? itemsValue : [];
}
// Note: AcpSessionManager resolves `sendRequest` with the JSON-RPC
// "result" directly (not the full AcpResponse). Treat it as unknown
// and carefully narrow before accessing `items` to satisfy strict TS.
if (res && typeof res === 'object' && 'items' in res) {
const itemsValue = (res as { items?: unknown }).items;
items = Array.isArray(itemsValue)
? (itemsValue as Array<Record<string, unknown>>)
: [];
}
console.log(
@@ -464,8 +521,25 @@ export class QwenAgentManager {
);
// Include all types of records, not just user/assistant
// Narrow unknown JSONL rows into a minimal shape we can work with.
type JsonlRecord = {
type: string;
timestamp: string;
message?: unknown;
toolCallResult?: { callId?: string; status?: string } | unknown;
subtype?: string;
systemPayload?: { uiEvent?: Record<string, unknown> } | unknown;
plan?: { entries?: Array<Record<string, unknown>> } | unknown;
};
const isJsonlRecord = (x: unknown): x is JsonlRecord =>
typeof x === 'object' &&
x !== null &&
typeof (x as Record<string, unknown>).type === 'string' &&
typeof (x as Record<string, unknown>).timestamp === 'string';
const allRecords = records
.filter((r) => r && r.type && r.timestamp)
.filter(isJsonlRecord)
.sort(
(a, b) =>
new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime(),
@@ -485,7 +559,7 @@ export class QwenAgentManager {
// Handle tool call records that might have content we want to show
else if (r.type === 'tool_call' || r.type === 'tool_call_update') {
// Convert tool calls to messages if they have relevant content
const toolContent = this.extractToolCallContent(r);
const toolContent = this.extractToolCallContent(r as unknown);
if (toolContent) {
msgs.push({
role: 'assistant',
@@ -495,10 +569,17 @@ export class QwenAgentManager {
}
}
// Handle tool result records
else if (r.type === 'tool_result' && r.toolCallResult) {
const toolResult = r.toolCallResult;
const callId = toolResult.callId || 'unknown';
const status = toolResult.status || 'unknown';
else if (
r.type === 'tool_result' &&
r.toolCallResult &&
typeof r.toolCallResult === 'object'
) {
const toolResult = r.toolCallResult as {
callId?: string;
status?: string;
};
const callId = toolResult.callId ?? 'unknown';
const status = toolResult.status ?? 'unknown';
const resultText = `Tool Result (${callId}): ${status}`;
msgs.push({
role: 'assistant',
@@ -510,33 +591,48 @@ export class QwenAgentManager {
else if (
r.type === 'system' &&
r.subtype === 'ui_telemetry' &&
r.systemPayload?.uiEvent
r.systemPayload &&
typeof r.systemPayload === 'object' &&
'uiEvent' in r.systemPayload &&
(r.systemPayload as { uiEvent?: Record<string, unknown> }).uiEvent
) {
const uiEvent = r.systemPayload.uiEvent;
const uiEvent = (
r.systemPayload as {
uiEvent?: Record<string, unknown>;
}
).uiEvent as Record<string, unknown>;
let telemetryText = '';
if (
uiEvent['event.name'] &&
uiEvent['event.name'].includes('tool_call')
typeof uiEvent['event.name'] === 'string' &&
(uiEvent['event.name'] as string).includes('tool_call')
) {
const functionName = uiEvent.function_name || 'Unknown tool';
const status = uiEvent.status || 'unknown';
const duration = uiEvent.duration_ms
? ` (${uiEvent.duration_ms}ms)`
const functionName =
(uiEvent['function_name'] as string | undefined) ||
'Unknown tool';
const status =
(uiEvent['status'] as string | undefined) || 'unknown';
const duration =
typeof uiEvent['duration_ms'] === 'number'
? ` (${uiEvent['duration_ms']}ms)`
: '';
telemetryText = `Tool Call: ${functionName} - ${status}${duration}`;
} else if (
uiEvent['event.name'] &&
uiEvent['event.name'].includes('api_response')
typeof uiEvent['event.name'] === 'string' &&
(uiEvent['event.name'] as string).includes('api_response')
) {
const statusCode = uiEvent.status_code || 'unknown';
const duration = uiEvent.duration_ms
? ` (${uiEvent.duration_ms}ms)`
const statusCode =
(uiEvent['status_code'] as string | number | undefined) ||
'unknown';
const duration =
typeof uiEvent['duration_ms'] === 'number'
? ` (${uiEvent['duration_ms']}ms)`
: '';
telemetryText = `API Response: Status ${statusCode}${duration}`;
} else {
// Generic system telemetry
const eventName = uiEvent['event.name'] || 'Unknown event';
const eventName =
(uiEvent['event.name'] as string | undefined) || 'Unknown event';
telemetryText = `System Event: ${eventName}`;
}
@@ -549,8 +645,15 @@ export class QwenAgentManager {
}
}
// Handle plan entries
else if (r.type === 'plan' && r.plan) {
const planEntries = r.plan.entries || [];
else if (
r.type === 'plan' &&
r.plan &&
typeof r.plan === 'object' &&
'entries' in r.plan
) {
const planEntries =
((r.plan as { entries?: Array<Record<string, unknown>> })
.entries as Array<Record<string, unknown>> | undefined) || [];
if (planEntries.length > 0) {
const planText = planEntries
.map(
@@ -1245,6 +1348,33 @@ export class QwenAgentManager {
this.sessionUpdateHandler.updateCallbacks(this.callbacks);
}
/**
* Register initialize mode info callback
*/
onModeInfo(
callback: (info: {
currentModeId?: 'plan' | 'default' | 'auto-edit' | 'yolo';
availableModes?: Array<{
id: 'plan' | 'default' | 'auto-edit' | 'yolo';
name: string;
description: string;
}>;
}) => void,
): void {
this.callbacks.onModeInfo = callback;
this.sessionUpdateHandler.updateCallbacks(this.callbacks);
}
/**
* Register mode changed callback
*/
onModeChanged(
callback: (modeId: 'plan' | 'default' | 'auto-edit' | 'yolo') => void,
): void {
this.callbacks.onModeChanged = callback;
this.sessionUpdateHandler.updateCallbacks(this.callbacks);
}
/**
* Disconnect
*/

View File

@@ -10,7 +10,10 @@
* Handles session updates from ACP and dispatches them to appropriate callbacks
*/
import type { AcpSessionUpdate } from '../constants/acpTypes.js';
import type {
AcpSessionUpdate,
ApprovalModeValue,
} from '../constants/acpTypes.js';
import type { QwenAgentCallbacks } from './qwenTypes.js';
/**
@@ -149,6 +152,23 @@ export class QwenSessionUpdateHandler {
break;
}
case 'current_mode_update': {
// Notify UI about mode change
try {
const modeId = (update as unknown as { modeId?: ApprovalModeValue })
.modeId;
if (modeId && this.callbacks.onModeChanged) {
this.callbacks.onModeChanged(modeId);
}
} catch (err) {
console.warn(
'[SessionUpdateHandler] Failed to handle mode update',
err,
);
}
break;
}
default:
console.log('[QwenAgentManager] Unhandled session update type');
break;

View File

@@ -61,4 +61,15 @@ export interface QwenAgentCallbacks {
onPermissionRequest?: (request: AcpPermissionRequest) => Promise<string>;
/** End of turn callback (e.g., stopReason === 'end_turn') */
onEndTurn?: () => void;
/** Initialize modes & capabilities info from ACP initialize */
onModeInfo?: (info: {
currentModeId?: 'plan' | 'default' | 'auto-edit' | 'yolo';
availableModes?: Array<{
id: 'plan' | 'default' | 'auto-edit' | 'yolo';
name: string;
description: string;
}>;
}) => void;
/** Mode changed notification */
onModeChanged?: (modeId: 'plan' | 'default' | 'auto-edit' | 'yolo') => void;
}

View File

@@ -9,7 +9,6 @@ export const showDiffCommand = 'qwenCode.showDiff';
export const openChatCommand = 'qwen-code.openChat';
export const openNewChatTabCommand = 'qwenCode.openNewChatTab';
export const loginCommand = 'qwen-code.login';
export const clearAuthCacheCommand = 'qwen-code.clearAuthCache';
export function registerNewCommands(
context: vscode.ExtensionContext,
@@ -90,19 +89,5 @@ export function registerNewCommands(
}
}),
);
disposables.push(
vscode.commands.registerCommand(clearAuthCacheCommand, async () => {
const providers = getWebViewProviders();
for (const provider of providers) {
await provider.clearAuthCache();
}
vscode.window.showInformationMessage(
'Qwen Code authentication cache cleared. You will need to login again on next connection.',
);
log('Auth cache cleared by user');
}),
);
context.subscriptions.push(...disposables);
}

View File

@@ -13,6 +13,7 @@ export const AGENT_METHODS = {
session_new: 'session/new',
session_prompt: 'session/prompt',
session_save: 'session/save',
session_set_mode: 'session/set_mode',
} as const;
export const CLIENT_METHODS = {

View File

@@ -153,6 +153,17 @@ export interface PlanUpdate extends BaseSessionUpdate {
};
}
// Approval/Mode values as defined by ACP schema
export type ApprovalModeValue = 'plan' | 'default' | 'auto-edit' | 'yolo';
// Current mode update (sent by agent when mode changes)
export interface CurrentModeUpdate extends BaseSessionUpdate {
update: {
sessionUpdate: 'current_mode_update';
modeId: ApprovalModeValue;
};
}
// Union type for all session updates
export type AcpSessionUpdate =
| UserMessageChunkUpdate
@@ -160,7 +171,8 @@ export type AcpSessionUpdate =
| AgentThoughtChunkUpdate
| ToolCallUpdate
| ToolCallStatusUpdate
| PlanUpdate;
| PlanUpdate
| CurrentModeUpdate;
// Permission request (simplified version, use schema.RequestPermissionRequest for validation)
export interface AcpPermissionRequest {

View File

@@ -61,11 +61,26 @@ export class DiffManager {
readonly onDidChange = this.onDidChangeEmitter.event;
private diffDocuments = new Map<string, DiffInfo>();
private readonly subscriptions: vscode.Disposable[] = [];
// Dedupe: remember recent showDiff calls keyed by (file+content)
private recentlyShown = new Map<string, number>();
private pendingDelayTimers = new Map<string, NodeJS.Timeout>();
private static readonly DEDUPE_WINDOW_MS = 1500;
// Optional hooks from extension to influence diff behavior
// - shouldDelay: when true, we defer opening diffs briefly (e.g., while a permission drawer is open)
// - shouldSuppress: when true, we skip opening diffs entirely (e.g., in auto/yolo mode)
private shouldDelay?: () => boolean;
private shouldSuppress?: () => boolean;
// Timed suppression window (e.g. immediately after permission allow)
private suppressUntil: number | null = null;
constructor(
private readonly log: (message: string) => void,
private readonly diffContentProvider: DiffContentProvider,
shouldDelay?: () => boolean,
shouldSuppress?: () => boolean,
) {
this.shouldDelay = shouldDelay;
this.shouldSuppress = shouldSuppress;
this.subscriptions.push(
vscode.window.onDidChangeActiveTextEditor((editor) => {
this.onActiveEditorChange(editor);
@@ -110,9 +125,10 @@ export class DiffManager {
* @returns True if an existing diff view was found and focused, false otherwise
*/
private async focusExistingDiff(filePath: string): Promise<boolean> {
for (const [uriString, diffInfo] of this.diffDocuments.entries()) {
if (diffInfo.originalFilePath === filePath) {
const rightDocUri = vscode.Uri.parse(uriString);
const normalizedPath = path.normalize(filePath);
for (const [, diffInfo] of this.diffDocuments.entries()) {
if (diffInfo.originalFilePath === normalizedPath) {
const rightDocUri = diffInfo.rightDocUri;
const leftDocUri = diffInfo.leftDocUri;
const diffTitle = `${path.basename(filePath)} (Before ↔ After)`;
@@ -126,7 +142,7 @@ export class DiffManager {
{
viewColumn: vscode.ViewColumn.Beside,
preview: false,
preserveFocus: false,
preserveFocus: true,
},
);
return true;
@@ -146,19 +162,70 @@ export class DiffManager {
* @param newContent The modified content (right side)
*/
async showDiff(filePath: string, oldContent: string, newContent: string) {
const normalizedPath = path.normalize(filePath);
const key = this.makeKey(normalizedPath, oldContent, newContent);
// Suppress entirely when the extension indicates diffs should not be shown
if (this.shouldSuppress && this.shouldSuppress()) {
this.log(`showDiff suppressed by policy for ${filePath}`);
return;
}
// Suppress during timed window
if (this.suppressUntil && Date.now() < this.suppressUntil) {
this.log(`showDiff suppressed by timed window for ${filePath}`);
return;
}
// If permission drawer is currently open, delay to avoid double-open
if (this.shouldDelay && this.shouldDelay()) {
if (!this.pendingDelayTimers.has(key)) {
const timer = setTimeout(() => {
this.pendingDelayTimers.delete(key);
// Fire and forget; rely on dedupe below to avoid double focus
void this.showDiff(filePath, oldContent, newContent);
}, 300);
this.pendingDelayTimers.set(key, timer);
}
return;
}
// If a diff tab for the same file is already open, update its content instead of opening a new one
for (const [, diffInfo] of this.diffDocuments.entries()) {
if (diffInfo.originalFilePath === normalizedPath) {
// Update left/right contents
this.diffContentProvider.setContent(diffInfo.leftDocUri, oldContent);
this.diffContentProvider.setContent(diffInfo.rightDocUri, newContent);
// Update stored snapshot for future comparisons
diffInfo.oldContent = oldContent;
diffInfo.newContent = newContent;
this.recentlyShown.set(key, Date.now());
// Soft focus existing (preserve chat focus)
await this.focusExistingDiff(normalizedPath);
return;
}
}
// Check if a diff view with the same content already exists
if (this.hasExistingDiff(filePath, oldContent, newContent)) {
if (this.hasExistingDiff(normalizedPath, oldContent, newContent)) {
const last = this.recentlyShown.get(key) || 0;
const now = Date.now();
if (now - last < DiffManager.DEDUPE_WINDOW_MS) {
// Within dedupe window: ignore the duplicate request entirely
this.log(
`Diff view already exists for ${filePath}, focusing existing view`,
`Duplicate showDiff suppressed for ${filePath} (within ${DiffManager.DEDUPE_WINDOW_MS}ms)`,
);
// Focus the existing diff view
await this.focusExistingDiff(filePath);
return;
}
// Outside the dedupe window: softly focus the existing diff
await this.focusExistingDiff(normalizedPath);
this.recentlyShown.set(key, now);
return;
}
// Left side: old content using qwen-diff scheme
const leftDocUri = vscode.Uri.from({
scheme: DIFF_SCHEME,
path: filePath,
path: normalizedPath,
query: `old&rand=${Math.random()}`,
});
this.diffContentProvider.setContent(leftDocUri, oldContent);
@@ -166,20 +233,20 @@ export class DiffManager {
// Right side: new content using qwen-diff scheme
const rightDocUri = vscode.Uri.from({
scheme: DIFF_SCHEME,
path: filePath,
path: normalizedPath,
query: `new&rand=${Math.random()}`,
});
this.diffContentProvider.setContent(rightDocUri, newContent);
this.addDiffDocument(rightDocUri, {
originalFilePath: filePath,
originalFilePath: normalizedPath,
oldContent,
newContent,
leftDocUri,
rightDocUri,
});
const diffTitle = `${path.basename(filePath)} (Before ↔ After)`;
const diffTitle = `${path.basename(normalizedPath)} (Before ↔ After)`;
await vscode.commands.executeCommand(
'setContext',
'qwen.diff.isVisible',
@@ -215,16 +282,19 @@ export class DiffManager {
await vscode.commands.executeCommand(
'workbench.action.files.setActiveEditorWriteableInSession',
);
this.recentlyShown.set(key, Date.now());
}
/**
* Closes an open diff view for a specific file.
*/
async closeDiff(filePath: string, suppressNotification = false) {
const normalizedPath = path.normalize(filePath);
let uriToClose: vscode.Uri | undefined;
for (const [uriString, diffInfo] of this.diffDocuments.entries()) {
if (diffInfo.originalFilePath === filePath) {
uriToClose = vscode.Uri.parse(uriString);
for (const [, diffInfo] of this.diffDocuments.entries()) {
if (diffInfo.originalFilePath === normalizedPath) {
uriToClose = diffInfo.rightDocUri;
break;
}
}
@@ -355,4 +425,29 @@ export class DiffManager {
}
}
}
/** Close all open qwen-diff editors */
async closeAll(): Promise<void> {
// Collect keys first to avoid iterator invalidation while closing
const uris = Array.from(this.diffDocuments.keys()).map((k) =>
vscode.Uri.parse(k),
);
for (const uri of uris) {
try {
await this.closeDiffEditor(uri);
} catch (err) {
this.log(`Failed to close diff editor: ${err}`);
}
}
}
private makeKey(filePath: string, oldContent: string, newContent: string) {
// Simple stable key; content could be large but kept transiently
return `${filePath}\u241F${oldContent}\u241F${newContent}`;
}
/** Temporarily suppress opening diffs for a short duration. */
suppressFor(durationMs: number): void {
this.suppressUntil = Date.now() + Math.max(0, durationMs);
}
}

View File

@@ -111,7 +111,20 @@ export async function activate(context: vscode.ExtensionContext) {
checkForUpdates(context, log);
const diffContentProvider = new DiffContentProvider();
const diffManager = new DiffManager(log, diffContentProvider);
const diffManager = new DiffManager(
log,
diffContentProvider,
// Delay when any chat tab has a pending permission drawer
() => webViewProviders.some((p) => p.hasPendingPermission()),
// Suppress diffs when active mode is auto or yolo in any chat tab
() => {
const providers = webViewProviders.filter(
(p) => typeof p.shouldSuppressDiff === 'function',
);
if (providers.length === 0) return false;
return providers.every((p) => p.shouldSuppressDiff());
},
);
// Helper function to create a new WebView provider instance
const createWebViewProvider = (): WebViewProvider => {
@@ -176,12 +189,21 @@ export async function activate(context: vscode.ExtensionContext) {
DIFF_SCHEME,
diffContentProvider,
),
vscode.commands.registerCommand('qwen.diff.accept', (uri?: vscode.Uri) => {
(vscode.commands.registerCommand('qwen.diff.accept', (uri?: vscode.Uri) => {
const docUri = uri ?? vscode.window.activeTextEditor?.document.uri;
if (docUri && docUri.scheme === DIFF_SCHEME) {
diffManager.acceptDiff(docUri);
}
// TODO: 如果 webview 在 request_permission 需要回复 cli
// 如果 WebView 在 request_permission,主动选择一个允许选项(优先 once
try {
for (const provider of webViewProviders) {
if (provider?.hasPendingPermission()) {
provider.respondToPendingPermission('allow');
}
}
} catch (err) {
console.warn('[Extension] Auto-allow on diff.accept failed:', err);
}
console.log('[Extension] Diff accepted');
}),
vscode.commands.registerCommand('qwen.diff.cancel', (uri?: vscode.Uri) => {
@@ -189,8 +211,31 @@ export async function activate(context: vscode.ExtensionContext) {
if (docUri && docUri.scheme === DIFF_SCHEME) {
diffManager.cancelDiff(docUri);
}
// TODO: 如果 webview 在 request_permission 需要回复 cli
// 如果 WebView 在 request_permission,主动选择拒绝/取消
try {
for (const provider of webViewProviders) {
if (provider?.hasPendingPermission()) {
provider.respondToPendingPermission('cancel');
}
}
} catch (err) {
console.warn('[Extension] Auto-reject on diff.cancel failed:', err);
}
console.log('[Extension] Diff cancelled');
})),
vscode.commands.registerCommand('qwen.diff.closeAll', async () => {
try {
await diffManager.closeAll();
} catch (err) {
console.warn('[Extension] qwen.diff.closeAll failed:', err);
}
}),
vscode.commands.registerCommand('qwen.diff.suppressBriefly', async () => {
try {
diffManager.suppressFor(1200);
} catch (err) {
console.warn('[Extension] qwen.diff.suppressBriefly failed:', err);
}
}),
);

View File

@@ -471,15 +471,27 @@ export const App: React.FC = () => {
});
}, [vscode]);
// Handle toggle edit mode
// Handle toggle edit mode (Ask -> Auto -> Plan -> YOLO -> Ask)
const handleToggleEditMode = useCallback(() => {
setEditMode((prev) => {
const next: EditMode =
prev === 'ask' ? 'auto' : prev === 'auto' ? 'plan' : 'ask';
prev === 'ask'
? 'auto'
: prev === 'auto'
? 'plan'
: prev === 'plan'
? 'yolo'
: 'ask';
// Notify extension to set approval mode via ACP
try {
const toAcp =
next === 'plan' ? 'plan' : next === 'auto' ? 'auto-edit' : 'default';
next === 'plan'
? 'plan'
: next === 'auto'
? 'auto-edit'
: next === 'yolo'
? 'yolo'
: 'default';
vscode.postMessage({
type: 'setApprovalMode',
data: { modeId: toAcp },

View File

@@ -26,6 +26,14 @@ export class WebViewProvider {
private authStateManager: AuthStateManager;
private disposables: vscode.Disposable[] = [];
private agentInitialized = false; // Track if agent has been initialized
// Track a pending permission request and its resolver so extension commands
// can "simulate" user choice from the command palette (e.g. after accepting
// a diff, auto-allow read/execute, or auto-reject on cancel).
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;
constructor(
context: vscode.ExtensionContext,
@@ -84,6 +92,38 @@ export class WebViewProvider {
});
});
// Surface available modes and current mode (from ACP initialize)
this.agentManager.onModeInfo((info) => {
try {
const current = (info?.currentModeId || null) as
| 'plan'
| 'default'
| 'auto-edit'
| 'yolo'
| null;
this.currentModeId = current;
} catch (_error) {
// Ignore error when parsing mode info
}
this.sendMessageToWebView({
type: 'modeInfo',
data: info || {},
});
});
// Surface mode changes (from ACP or immediate set_mode response)
this.agentManager.onModeChanged((modeId) => {
try {
this.currentModeId = modeId;
} catch (_error) {
// Ignore error when setting mode id
}
this.sendMessageToWebView({
type: 'modeChanged',
data: { modeId },
});
});
// Setup end-turn handler from ACP stopReason=end_turn
this.agentManager.onEndTurn(() => {
// Ensure WebView exits streaming state even if no explicit streamEnd was emitted elsewhere
@@ -140,6 +180,25 @@ export class WebViewProvider {
this.agentManager.onPermissionRequest(
async (request: AcpPermissionRequest) => {
// Auto-approve in auto/yolo mode (no UI, no diff)
if (this.isAutoMode()) {
const options = request.options || [];
const pick = (substr: string) =>
options.find((o) =>
(o.optionId || '').toLowerCase().includes(substr),
)?.optionId;
const pickByKind = (k: string) =>
options.find((o) => (o.kind || '').toLowerCase().includes(k))
?.optionId;
const optionId =
pick('allow_once') ||
pickByKind('allow') ||
pick('proceed') ||
options[0]?.optionId ||
'allow_once';
return optionId;
}
// Send permission request to WebView
this.sendMessageToWebView({
type: 'permissionRequest',
@@ -148,16 +207,58 @@ export class WebViewProvider {
// Wait for user response
return new Promise((resolve) => {
// cache the pending request and its resolver so commands can resolve it
this.pendingPermissionRequest = request;
this.pendingPermissionResolve = (optionId: string) => {
try {
resolve(optionId);
} finally {
// Always clear pending state
this.pendingPermissionRequest = null;
this.pendingPermissionResolve = null;
// Also instruct the webview UI to close its drawer if it is open
this.sendMessageToWebView({
type: 'permissionResolved',
data: { optionId },
});
// If allowed/proceeded, close any open qwen-diff editors and suppress re-open briefly
const isCancel =
optionId === 'cancel' ||
optionId.toLowerCase().includes('reject');
if (!isCancel) {
try {
void vscode.commands.executeCommand('qwen.diff.closeAll');
} catch (err) {
console.warn(
'[WebViewProvider] Failed to close diffs after allow (resolver):',
err,
);
}
try {
void vscode.commands.executeCommand(
'qwen.diff.suppressBriefly',
);
} catch (err) {
console.warn(
'[WebViewProvider] Failed to suppress diffs briefly:',
err,
);
}
}
}
};
const handler = (message: {
type: string;
data: { optionId: string };
}) => {
if (message.type !== 'permissionResponse') return;
if (message.type !== 'permissionResponse') {
return;
}
const optionId = message.data.optionId || '';
// 1) First resolve the optionId back to ACP so the agent isn't blocked
resolve(optionId);
this.pendingPermissionResolve?.(optionId);
// 2) If user cancelled/rejected, proactively stop current generation
const isCancel =
@@ -197,10 +298,13 @@ export class WebViewProvider {
)?.kind || 'execute') as string;
if (!kind && title) {
const t = title.toLowerCase();
if (t.includes('read') || t.includes('cat')) kind = 'read';
else if (t.includes('write') || t.includes('edit'))
if (t.includes('read') || t.includes('cat')) {
kind = 'read';
} else if (t.includes('write') || t.includes('edit')) {
kind = 'edit';
else kind = 'execute';
} else {
kind = 'execute';
}
}
this.sendMessageToWebView({
@@ -232,6 +336,27 @@ export class WebViewProvider {
}
})();
}
// If user allowed/proceeded, proactively close any open qwen-diff editors and suppress re-open briefly
else {
try {
void vscode.commands.executeCommand('qwen.diff.closeAll');
} catch (err) {
console.warn(
'[WebViewProvider] Failed to close diffs after allow:',
err,
);
}
try {
void vscode.commands.executeCommand(
'qwen.diff.suppressBriefly',
);
} catch (err) {
console.warn(
'[WebViewProvider] Failed to suppress diffs briefly:',
err,
);
}
}
};
// Store handler in message handler
this.messageHandler.setPermissionHandler(handler);
@@ -283,6 +408,10 @@ export class WebViewProvider {
// Handle messages from WebView
newPanel.webview.onDidReceiveMessage(
async (message: { type: string; data?: unknown }) => {
// Suppress UI-originated diff opens in auto/yolo mode
if (message.type === 'openDiff' && this.isAutoMode()) {
return;
}
// Allow webview to request updating the VS Code tab title
if (message.type === 'updatePanelTitle') {
const title = String(
@@ -306,16 +435,6 @@ export class WebViewProvider {
// Register panel dispose handler
this.panelManager.registerDisposeHandler(this.disposables);
// Track last known editor state (to preserve when switching to webview)
let _lastEditorState: {
fileName: string | null;
filePath: string | null;
selection: {
startLine: number;
endLine: number;
} | null;
} | null = null;
// Listen for active editor changes and notify WebView
const editorChangeDisposable = vscode.window.onDidChangeActiveTextEditor(
(editor) => {
@@ -339,7 +458,6 @@ export class WebViewProvider {
}
// Update last known state
_lastEditorState = { fileName, filePath, selection: selectionInfo };
this.sendMessageToWebView({
type: 'activeEditorChanged',
@@ -368,28 +486,13 @@ export class WebViewProvider {
}
// Update last known state
_lastEditorState = { fileName, filePath, selection: selectionInfo };
this.sendMessageToWebView({
type: 'activeEditorChanged',
data: { fileName, filePath, selection: selectionInfo },
});
// Surface available modes and current mode (from ACP initialize)
this.agentManager.onModeInfo((info) => {
this.sendMessageToWebView({
type: 'modeInfo',
data: info || {},
});
});
// Surface mode changes (from ACP or immediate set_mode response)
this.agentManager.onModeChanged((modeId) => {
this.sendMessageToWebView({
type: 'modeChanged',
data: { modeId },
});
});
// Mode callbacks are registered in constructor; no-op here
}
});
this.disposables.push(selectionChangeDisposable);
@@ -459,8 +562,8 @@ export class WebViewProvider {
);
await this.initializeEmptyConversation();
}
} catch (error) {
console.error('[WebViewProvider] Auth state restoration failed:', error);
} catch (_error) {
console.error('[WebViewProvider] Auth state restoration failed:', _error);
// Fallback to rendering empty conversation
await this.initializeEmptyConversation();
}
@@ -530,12 +633,12 @@ export class WebViewProvider {
type: 'agentConnected',
data: {},
});
} catch (error) {
console.error('[WebViewProvider] Agent connection error:', error);
} catch (_error) {
console.error('[WebViewProvider] Agent connection error:', _error);
// Clear auth cache on error (might be auth issue)
await this.authStateManager.clearAuthState();
vscode.window.showWarningMessage(
`Failed to connect to Qwen CLI: ${error}\nYou can still use the chat UI, but messages won't be sent to AI.`,
`Failed to connect to Qwen CLI: ${_error}\nYou can still use the chat UI, but messages won't be sent to AI.`,
);
// Fallback to empty conversation
await this.initializeEmptyConversation();
@@ -544,7 +647,7 @@ export class WebViewProvider {
this.sendMessageToWebView({
type: 'agentConnectionError',
data: {
message: error instanceof Error ? error.message : String(error),
message: _error instanceof Error ? _error.message : String(_error),
},
});
}
@@ -585,8 +688,8 @@ export class WebViewProvider {
try {
this.agentManager.disconnect();
console.log('[WebViewProvider] Existing connection disconnected');
} catch (error) {
console.log('[WebViewProvider] Error disconnecting:', error);
} catch (_error) {
console.log('[WebViewProvider] Error disconnecting:', _error);
}
this.agentInitialized = false;
}
@@ -617,22 +720,22 @@ export class WebViewProvider {
type: 'loginSuccess',
data: { message: 'Successfully logged in!' },
});
} catch (error) {
console.error('[WebViewProvider] Force re-login failed:', error);
} catch (_error) {
console.error('[WebViewProvider] Force re-login failed:', _error);
console.error(
'[WebViewProvider] Error stack:',
error instanceof Error ? error.stack : 'N/A',
_error instanceof Error ? _error.stack : 'N/A',
);
// Send error notification to WebView
this.sendMessageToWebView({
type: 'loginError',
data: {
message: `Login failed: ${error instanceof Error ? error.message : String(error)}`,
message: `Login failed: ${_error instanceof Error ? _error.message : String(_error)}`,
},
});
throw error;
throw _error;
}
},
);
@@ -650,8 +753,8 @@ export class WebViewProvider {
try {
this.agentManager.disconnect();
console.log('[WebViewProvider] Existing connection disconnected');
} catch (error) {
console.log('[WebViewProvider] Error disconnecting:', error);
} catch (_error) {
console.log('[WebViewProvider] Error disconnecting:', _error);
}
this.agentInitialized = false;
}
@@ -671,18 +774,18 @@ export class WebViewProvider {
type: 'agentConnected',
data: {},
});
} catch (error) {
console.error('[WebViewProvider] Connection refresh failed:', error);
} catch (_error) {
console.error('[WebViewProvider] Connection refresh failed:', _error);
// Notify webview that agent connection failed after refresh
this.sendMessageToWebView({
type: 'agentConnectionError',
data: {
message: error instanceof Error ? error.message : String(error),
message: _error instanceof Error ? _error.message : String(_error),
},
});
throw error;
throw _error;
}
}
@@ -725,13 +828,13 @@ export class WebViewProvider {
}
await this.initializeEmptyConversation();
} catch (error) {
} catch (_error) {
console.error(
'[WebViewProvider] Failed to load session messages:',
error,
_error,
);
vscode.window.showErrorMessage(
`Failed to load session messages: ${error}`,
`Failed to load session messages: ${_error}`,
);
await this.initializeEmptyConversation();
}
@@ -754,10 +857,10 @@ export class WebViewProvider {
'[WebViewProvider] Empty conversation initialized:',
this.messageHandler.getCurrentConversationId(),
);
} catch (error) {
} catch (_error) {
console.error(
'[WebViewProvider] Failed to initialize conversation:',
error,
_error,
);
// Send empty state to WebView as fallback
this.sendMessageToWebView({
@@ -775,6 +878,100 @@ export class WebViewProvider {
panel?.webview.postMessage(message);
}
/**
* Whether there is a pending permission decision awaiting an option.
*/
hasPendingPermission(): boolean {
return !!this.pendingPermissionResolve;
}
/** Get current ACP mode id (if known). */
getCurrentModeId(): 'plan' | 'default' | 'auto-edit' | 'yolo' | null {
return this.currentModeId;
}
/** True if diffs/permissions should be auto-handled without prompting. */
isAutoMode(): boolean {
return this.currentModeId === 'auto-edit' || this.currentModeId === 'yolo';
}
/** Used by extension to decide if diffs should be suppressed. */
shouldSuppressDiff(): boolean {
return this.isAutoMode();
}
/**
* Simulate selecting a permission option while a request drawer is open.
* The choice can be a concrete optionId or a shorthand intent.
*/
respondToPendingPermission(
choice: { optionId: string } | 'accept' | 'allow' | 'reject' | 'cancel',
): void {
if (!this.pendingPermissionResolve || !this.pendingPermissionRequest) {
return; // nothing to do
}
const options = this.pendingPermissionRequest.options || [];
const pickByKind = (substr: string, preferOnce = false) => {
const lc = substr.toLowerCase();
const filtered = options.filter((o) =>
(o.kind || '').toLowerCase().includes(lc),
);
if (preferOnce) {
const once = filtered.find((o) =>
(o.optionId || '').toLowerCase().includes('once'),
);
if (once) {
return once.optionId;
}
}
return filtered[0]?.optionId;
};
const pickByOptionId = (substr: string) =>
options.find((o) => (o.optionId || '').toLowerCase().includes(substr))
?.optionId;
let optionId: string | undefined;
if (typeof choice === 'object') {
optionId = choice.optionId;
} else {
const c = choice.toLowerCase();
if (c === 'accept' || c === 'allow') {
// Prefer an allow_once/proceed_once style option, then any allow/proceed
optionId =
pickByKind('allow', true) ||
pickByOptionId('proceed_once') ||
pickByKind('allow') ||
pickByOptionId('proceed') ||
options[0]?.optionId; // last resort: first option
} else if (c === 'cancel' || c === 'reject') {
// Prefer explicit cancel, then a reject option
optionId =
options.find((o) => o.optionId === 'cancel')?.optionId ||
pickByKind('reject') ||
pickByOptionId('cancel') ||
pickByOptionId('reject') ||
'cancel';
}
}
if (!optionId) {
return;
}
try {
this.pendingPermissionResolve(optionId);
} catch (_error) {
console.warn(
'[WebViewProvider] respondToPendingPermission failed:',
_error,
);
}
}
/**
* Reset agent initialization state
* Call this when auth cache is cleared to force re-authentication
@@ -824,6 +1021,10 @@ export class WebViewProvider {
// Handle messages from WebView (restored panel)
panel.webview.onDidReceiveMessage(
async (message: { type: string; data?: unknown }) => {
// Suppress UI-originated diff opens in auto/yolo mode
if (message.type === 'openDiff' && this.isAutoMode()) {
return;
}
if (message.type === 'updatePanelTitle') {
const title = String(
(message.data as { title?: unknown } | undefined)?.title ?? '',
@@ -846,16 +1047,6 @@ export class WebViewProvider {
// Register dispose handler
this.panelManager.registerDisposeHandler(this.disposables);
// Track last known editor state (to preserve when switching to webview)
let _lastEditorState: {
fileName: string | null;
filePath: string | null;
selection: {
startLine: number;
endLine: number;
} | null;
} | null = null;
// Listen for active editor changes and notify WebView
const editorChangeDisposable = vscode.window.onDidChangeActiveTextEditor(
(editor) => {
@@ -879,7 +1070,6 @@ export class WebViewProvider {
}
// Update last known state
_lastEditorState = { fileName, filePath, selection: selectionInfo };
this.sendMessageToWebView({
type: 'activeEditorChanged',
@@ -929,7 +1119,6 @@ export class WebViewProvider {
}
// Update last known state
_lastEditorState = { fileName, filePath, selection: selectionInfo };
this.sendMessageToWebView({
type: 'activeEditorChanged',
@@ -1021,13 +1210,13 @@ export class WebViewProvider {
try {
await vscode.commands.executeCommand(runQwenCodeCommand);
console.log('[WebViewProvider] Opened new terminal session');
} catch (error) {
} catch (_error) {
console.error(
'[WebViewProvider] Failed to open new terminal session:',
error,
_error,
);
vscode.window.showErrorMessage(
`Failed to open new terminal session: ${error}`,
`Failed to open new terminal session: ${_error}`,
);
}
return;
@@ -1051,9 +1240,9 @@ export class WebViewProvider {
});
console.log('[WebViewProvider] New session created successfully');
} catch (error) {
console.error('[WebViewProvider] Failed to create new session:', error);
vscode.window.showErrorMessage(`Failed to create new session: ${error}`);
} catch (_error) {
console.error('[WebViewProvider] Failed to create new session:', _error);
vscode.window.showErrorMessage(`Failed to create new session: ${_error}`);
}
}

View File

@@ -20,7 +20,7 @@ import {
import { CompletionMenu } from './ui/CompletionMenu.js';
import type { CompletionItem } from '../types/CompletionTypes.js';
type EditMode = 'ask' | 'auto' | 'plan';
type EditMode = 'ask' | 'auto' | 'plan' | 'yolo';
interface InputFormProps {
inputText: string;
@@ -75,6 +75,13 @@ const getEditModeInfo = (editMode: EditMode) => {
title: 'Qwen will plan before executing. Click to switch modes.',
icon: <PlanModeIcon />,
};
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: <AutoEditIcon />,
};
default:
return {
text: 'Unknown mode',

View File

@@ -165,8 +165,7 @@
}
.markdown-content .code-block-wrapper pre {
/* Reserve space so the copy button never overlaps code text */
padding-top: 1.5rem; /* Reduced padding - room for the button height */
padding-top: 1rem; /* Reduced padding - room for the button height */
padding-right: 2rem; /* Reduced padding - room for the button width */
}

View File

@@ -92,7 +92,13 @@ export class SettingsMessageHandler extends BaseMessageHandler {
| 'auto-edit'
| 'yolo';
await this.agentManager.setApprovalModeFromUi(
modeId === 'plan' ? 'plan' : modeId === 'auto-edit' ? 'auto' : 'ask',
modeId === 'plan'
? 'plan'
: modeId === 'auto-edit'
? 'auto'
: modeId === 'yolo'
? 'yolo'
: 'ask',
);
// No explicit response needed; WebView listens for modeChanged
} catch (error) {

View File

@@ -11,7 +11,7 @@ import type {
PermissionOption,
ToolCall as PermissionToolCall,
} from '../components/PermissionDrawer/PermissionRequest.js';
import type { ToolCallUpdate } from '../types/toolCall.js';
import type { ToolCallUpdate, EditMode } from '../types/toolCall.js';
import type { PlanEntry } from '../../agents/qwenTypes.js';
interface UseWebViewMessagesProps {
@@ -94,14 +94,20 @@ interface UseWebViewMessagesProps {
setPlanEntries: (entries: PlanEntry[]) => void;
// Permission
handlePermissionRequest: (request: {
// When request is non-null, open/update the permission drawer.
// When null, close the drawer (used when extension simulates a choice).
handlePermissionRequest: (
request: {
options: PermissionOption[];
toolCall: PermissionToolCall;
}) => void;
} | null,
) => void;
// Input
inputFieldRef: React.RefObject<HTMLDivElement>;
setInputText: (text: string) => void;
// Edit mode setter (maps ACP modes to UI modes)
setEditMode?: (mode: EditMode) => void;
}
/**
@@ -118,6 +124,7 @@ export const useWebViewMessages = ({
handlePermissionRequest,
inputFieldRef,
setInputText,
setEditMode,
}: UseWebViewMessagesProps) => {
// VS Code API for posting messages back to the extension host
const vscode = useVSCode();
@@ -186,6 +193,50 @@ export const useWebViewMessages = ({
const handlers = handlersRef.current;
switch (message.type) {
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,
);
} catch (_error) {
// best effort
}
break;
}
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,
);
} catch (_error) {
// Ignore error when setting mode
}
break;
}
case 'loginSuccess': {
// Clear loading state and show a short assistant notice
handlers.messageHandling.clearWaitingForResponse();
@@ -268,9 +319,9 @@ export const useWebViewMessages = ({
if (msg.role === 'assistant') {
try {
handlers.messageHandling.endStreaming();
} catch (err) {
} catch (_error) {
// no-op: stream might not have been started
console.warn('[PanelManager] Failed to end streaming:', err);
console.warn('[PanelManager] Failed to end streaming:', _error);
}
// Important: Do NOT blindly clear the waiting message if there are
// still active tool calls running. We keep the waiting indicator
@@ -278,11 +329,11 @@ export const useWebViewMessages = ({
if (activeExecToolCallsRef.current.size === 0) {
try {
handlers.messageHandling.clearWaitingForResponse();
} catch (err) {
} catch (_error) {
// no-op: already cleared
console.warn(
'[PanelManager] Failed to clear waiting for response:',
err,
_error,
);
}
}
@@ -307,15 +358,36 @@ export const useWebViewMessages = ({
break;
}
case 'streamEnd':
case 'streamEnd': {
// Always end local streaming state and collapse any thoughts
handlers.messageHandling.endStreaming();
handlers.messageHandling.clearThinking();
// Clear the generic waiting indicator only if there are no active
// long-running tool calls. Otherwise, keep it visible.
// If the stream ended due to explicit user cancel, proactively
// clear the waiting indicator and reset any tracked exec calls.
// This avoids the UI being stuck with the Stop button visible
// after rejecting a permission request.
try {
const reason = (
(message.data as { reason?: string } | undefined)?.reason || ''
).toLowerCase();
if (reason === 'user_cancelled') {
activeExecToolCallsRef.current.clear();
handlers.messageHandling.clearWaitingForResponse();
break;
}
} catch (_error) {
// best-effort
}
// Otherwise, clear the generic waiting indicator only if there are
// no active long-running tool calls. If there are still active
// execute/bash/command calls, keep the hint visible.
if (activeExecToolCallsRef.current.size === 0) {
handlers.messageHandling.clearWaitingForResponse();
}
break;
}
case 'error':
handlers.messageHandling.clearWaitingForResponse();
@@ -334,8 +406,22 @@ export const useWebViewMessages = ({
};
if (permToolCall?.toolCallId) {
// Infer kind more robustly for permission preview:
// - If content contains a diff entry, force 'edit' so the EditToolCall auto-opens VS Code diff
// - Else try title-based hints; fall back to provided kind or 'execute'
let kind = permToolCall.kind || 'execute';
if (permToolCall.title) {
const contentArr = (permToolCall.content as unknown[]) || [];
const hasDiff = Array.isArray(contentArr)
? contentArr.some(
(c: unknown) =>
!!c &&
typeof c === 'object' &&
(c as { type?: string }).type === 'diff',
)
: false;
if (hasDiff) {
kind = 'edit';
} else if (permToolCall.title) {
const title = permToolCall.title.toLowerCase();
if (title.includes('touch') || title.includes('echo')) {
kind = 'execute';
@@ -371,6 +457,19 @@ export const useWebViewMessages = ({
break;
}
case 'permissionResolved': {
// Extension proactively resolved a pending permission; close drawer.
try {
handlers.handlePermissionRequest(null);
} catch (_error) {
console.warn(
'[useWebViewMessages] failed to close permission UI:',
_error,
);
}
break;
}
case 'plan':
if (message.data.entries && Array.isArray(message.data.entries)) {
const entries = message.data.entries as PlanEntry[];
@@ -428,10 +527,10 @@ export const useWebViewMessages = ({
// Split assistant message segments, keep rendering blocks independent
handlers.messageHandling.breakAssistantSegment?.();
} catch (err) {
} catch (_error) {
console.warn(
'[useWebViewMessages] failed to push/merge plan snapshot toolcall:',
err,
_error,
);
}
}
@@ -489,7 +588,7 @@ export const useWebViewMessages = ({
handlers.messageHandling.clearWaitingForResponse();
}
}
} catch (_err) {
} catch (_error) {
// Best-effort UI hint; ignore errors
}
break;
@@ -554,6 +653,12 @@ export const useWebViewMessages = ({
handlers.messageHandling.clearMessages();
}
// Clear any waiting message that might be displayed from previous session
handlers.messageHandling.clearWaitingForResponse();
// Clear active tool calls tracking
activeExecToolCallsRef.current.clear();
// Clear and restore tool calls if provided in session data
handlers.clearToolCalls();
if (message.data.toolCalls && Array.isArray(message.data.toolCalls)) {
@@ -682,7 +787,7 @@ export const useWebViewMessages = ({
break;
}
},
[inputFieldRef, setInputText, vscode],
[inputFieldRef, setInputText, vscode, setEditMode],
);
useEffect(() => {

View File

@@ -36,4 +36,4 @@ export interface ToolCallUpdate {
/**
* Edit mode type
*/
export type EditMode = 'ask' | 'auto' | 'plan';
export type EditMode = 'ask' | 'auto' | 'plan' | 'yolo';