mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-22 09:47:47 +00:00
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:
@@ -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 },
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 */
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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: {
|
||||
options: PermissionOption[];
|
||||
toolCall: PermissionToolCall;
|
||||
}) => void;
|
||||
// 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;
|
||||
} | 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(() => {
|
||||
|
||||
@@ -36,4 +36,4 @@ export interface ToolCallUpdate {
|
||||
/**
|
||||
* Edit mode type
|
||||
*/
|
||||
export type EditMode = 'ask' | 'auto' | 'plan';
|
||||
export type EditMode = 'ask' | 'auto' | 'plan' | 'yolo';
|
||||
|
||||
Reference in New Issue
Block a user