fix(vscode-ide-companion): fix bugs & support terminal mode operation

This commit is contained in:
yiliang114
2025-12-06 00:30:22 +08:00
parent be44e7af56
commit 96b275a756
27 changed files with 515 additions and 626 deletions

View File

@@ -72,35 +72,10 @@
"configuration": {
"title": "Qwen Code",
"properties": {
"qwenCode.qwen.enabled": {
"qwenCode.useTerminal": {
"type": "boolean",
"default": true,
"description": "Enable Qwen agent integration"
},
"qwenCode.qwen.cliPath": {
"type": "string",
"default": "qwen",
"description": "Path to Qwen CLI executable"
},
"qwenCode.qwen.openaiApiKey": {
"type": "string",
"default": "",
"description": "OpenAI API key for Qwen (optional, if not using Code Assist)"
},
"qwenCode.qwen.openaiBaseUrl": {
"type": "string",
"default": "",
"description": "OpenAI base URL for custom endpoints (optional)"
},
"qwenCode.qwen.model": {
"type": "string",
"default": "",
"description": "Model to use (optional)"
},
"qwenCode.qwen.proxy": {
"type": "string",
"default": "",
"description": "Proxy for Qwen client (format: schema://user:password@host:port, e.g., http://127.0.0.1:7890)"
"default": "false",
"description": "Use terminal to run Qwen Code"
}
}
},

View File

@@ -16,7 +16,7 @@ import type {
AcpNotification,
AcpResponse,
} from '../constants/acpTypes.js';
import { AGENT_METHODS, CUSTOM_METHODS } from '../constants/acpSchema.js';
import { AGENT_METHODS } from '../constants/acpSchema.js';
import type { PendingRequest } from './connectionTypes.js';
import type { ChildProcess } from 'child_process';
@@ -306,7 +306,7 @@ export class AcpSessionManager {
console.log('[ACP] Requesting session list...');
try {
const response = await this.sendRequest<AcpResponse>(
CUSTOM_METHODS.session_list,
AGENT_METHODS.session_list,
{},
child,
pendingRequests,

View File

@@ -3,7 +3,6 @@
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import * as vscode from 'vscode';
import { AcpConnection } from '../acp/acpConnection.js';
import type {
AcpSessionUpdate,
@@ -24,6 +23,7 @@ import type {
import { QwenConnectionHandler } from './qwenConnectionHandler.js';
import { QwenSessionUpdateHandler } from './qwenSessionUpdateHandler.js';
import { CliContextManager } from '../cli/cliContextManager.js';
import { authMethod } from '../auth/index.js';
export type { ChatMessage, PlanEntry, ToolCallUpdateData };
@@ -682,9 +682,6 @@ export class QwenAgentManager {
// Check if we have valid cached authentication
let hasValidAuth = false;
const config = vscode.workspace.getConfiguration('qwenCode');
const openaiApiKey = config.get<string>('qwen.openaiApiKey', '');
const authMethod = openaiApiKey ? 'openai' : 'qwen-oauth';
// Prefer the provided authStateManager, otherwise fall back to the one
// remembered during connect(). This prevents accidental re-auth in
// fallback paths (e.g. session switching) when the handler didn't pass it.

View File

@@ -16,6 +16,7 @@ import type { QwenSessionReader } from '../services/qwenSessionReader.js';
import type { AuthStateManager } from '../auth/authStateManager.js';
import { CliVersionManager } from '../cli/cliVersionManager.js';
import { CliContextManager } from '../cli/cliContextManager.js';
import { authMethod } from '../auth/index.js';
/**
* Qwen Connection Handler class
@@ -66,47 +67,23 @@ export class QwenConnectionHandler {
// Use the provided CLI path if available, otherwise use the configured path
const effectiveCliPath =
cliPath || config.get<string>('qwen.cliPath', 'qwen');
const openaiApiKey = config.get<string>('qwen.openaiApiKey', '');
const openaiBaseUrl = config.get<string>('qwen.openaiBaseUrl', '');
const model = config.get<string>('qwen.model', '');
const proxy = config.get<string>('qwen.proxy', '');
// Build extra CLI arguments
// Build extra CLI arguments (only essential parameters)
const extraArgs: string[] = [];
if (openaiApiKey) {
extraArgs.push('--openai-api-key', openaiApiKey);
}
if (openaiBaseUrl) {
extraArgs.push('--openai-base-url', openaiBaseUrl);
}
if (model) {
extraArgs.push('--model', model);
}
if (proxy) {
extraArgs.push('--proxy', proxy);
console.log('[QwenAgentManager] Using proxy:', proxy);
}
await connection.connect('qwen', effectiveCliPath, workingDir, extraArgs);
// Determine authentication method
const authMethod = openaiApiKey ? 'openai' : 'qwen-oauth';
// Check if we have valid cached authentication
if (authStateManager) {
console.log('[QwenAgentManager] Checking for cached authentication...');
console.log('[QwenAgentManager] Working dir:', workingDir);
console.log('[QwenAgentManager] Auth method:', authMethod);
const hasValidAuth = await authStateManager.hasValidAuth(
workingDir,
authMethod,
);
console.log('[QwenAgentManager] Has valid auth:', hasValidAuth);
if (hasValidAuth) {
console.log('[QwenAgentManager] Using cached authentication');
} else {
console.log('[QwenAgentManager] No valid cached authentication found');
}
} else {
console.log('[QwenAgentManager] No authStateManager provided');
}

View File

@@ -0,0 +1,7 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
export const authMethod = 'qwen-oauth';

View File

@@ -9,7 +9,7 @@ import { CliDetector, type CliDetectionResult } from './cliDetector.js';
/**
* Minimum CLI version that supports session/list and session/load ACP methods
*/
export const MIN_CLI_VERSION_FOR_SESSION_METHODS = '0.2.4';
export const MIN_CLI_VERSION_FOR_SESSION_METHODS = '0.4.0';
/**
* CLI Feature Flags based on version

View File

@@ -4,6 +4,9 @@ import type { WebViewProvider } from '../webview/WebViewProvider.js';
type Logger = (message: string) => void;
export const showDiffCommand = 'qwenCode.showDiff';
export const openChatCommand = 'qwenCode.openChat';
export function registerNewCommands(
context: vscode.ExtensionContext,
log: Logger,
@@ -13,10 +16,33 @@ export function registerNewCommands(
): void {
const disposables: vscode.Disposable[] = [];
// qwenCode.showDiff
disposables.push(
vscode.commands.registerCommand(openChatCommand, async () => {
const config = vscode.workspace.getConfiguration('qwenCode');
const useTerminal = config.get<boolean>('useTerminal', false);
console.log('[Command] Using terminal mode:', useTerminal);
if (useTerminal) {
// 使用终端模式
await vscode.commands.executeCommand(
'qwen-code.runQwenCode',
vscode.TerminalLocation.Editor, // 在编辑器区域创建终端,
);
} else {
// 使用 WebView 模式
const providers = getWebViewProviders();
if (providers.length > 0) {
await providers[providers.length - 1].show();
} else {
const provider = createWebViewProvider();
await provider.show();
}
}
}),
);
disposables.push(
vscode.commands.registerCommand(
'qwenCode.showDiff',
showDiffCommand,
async (args: { path: string; oldText: string; newText: string }) => {
log(`[Command] showDiff called for: ${args.path}`);
try {
@@ -40,28 +66,14 @@ export function registerNewCommands(
),
);
// qwenCode.openChat
// TODO: qwenCode.openNewChatTab (not contributed in package.json; used programmatically)
disposables.push(
vscode.commands.registerCommand('qwenCode.openChat', () => {
const providers = getWebViewProviders();
if (providers.length > 0) {
providers[providers.length - 1].show();
} else {
const provider = createWebViewProvider();
provider.show();
}
}),
);
// qwenCode.openNewChatTab (not contributed in package.json; used programmatically)
disposables.push(
vscode.commands.registerCommand('qwenCode.openNewChatTab', () => {
vscode.commands.registerCommand('qwenCode.openNewChatTab', async () => {
const provider = createWebViewProvider();
provider.show();
await provider.show();
}),
);
// qwenCode.clearAuthCache
disposables.push(
vscode.commands.registerCommand('qwenCode.clearAuthCache', async () => {
const providers = getWebViewProviders();
@@ -75,7 +87,6 @@ export function registerNewCommands(
}),
);
// qwenCode.login
disposables.push(
vscode.commands.registerCommand('qwenCode.login', async () => {
const providers = getWebViewProviders();

View File

@@ -50,11 +50,3 @@ export const CLIENT_METHODS = {
session_request_permission: 'session/request_permission',
session_update: 'session/update',
} as const;
/**
* Custom methods (not in standard ACP protocol)
* These are VSCode extension specific extensions
*/
export const CUSTOM_METHODS = {
session_list: 'session/list',
} as const;

View File

@@ -46,7 +46,9 @@ export class DiffContentProvider implements vscode.TextDocumentContentProvider {
// Information about a diff view that is currently open.
interface DiffInfo {
originalFilePath: string;
oldContent: string;
newContent: string;
leftDocUri: vscode.Uri;
rightDocUri: vscode.Uri;
}
@@ -78,6 +80,65 @@ export class DiffManager {
}
}
/**
* Checks if a diff view already exists for the given file path and content
* @param filePath Path to the file being diffed
* @param oldContent The original content (left side)
* @param newContent The modified content (right side)
* @returns True if a diff view with the same content already exists, false otherwise
*/
private hasExistingDiff(
filePath: string,
oldContent: string,
newContent: string,
): boolean {
for (const diffInfo of this.diffDocuments.values()) {
if (
diffInfo.originalFilePath === filePath &&
diffInfo.oldContent === oldContent &&
diffInfo.newContent === newContent
) {
return true;
}
}
return false;
}
/**
* Finds an existing diff view for the given file path and focuses it
* @param filePath Path to the file being diffed
* @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 leftDocUri = diffInfo.leftDocUri;
const diffTitle = `${path.basename(filePath)} (Before ↔ After)`;
try {
await vscode.commands.executeCommand(
'vscode.diff',
leftDocUri,
rightDocUri,
diffTitle,
{
viewColumn: vscode.ViewColumn.Beside,
preview: false,
preserveFocus: false,
},
);
return true;
} catch (error) {
this.log(`Failed to focus existing diff: ${error}`);
return false;
}
}
}
return false;
}
/**
* Creates and shows a new diff view.
* @param filePath Path to the file being diffed
@@ -85,6 +146,15 @@ export class DiffManager {
* @param newContent The modified content (right side)
*/
async showDiff(filePath: string, oldContent: string, newContent: string) {
// Check if a diff view with the same content already exists
if (this.hasExistingDiff(filePath, oldContent, newContent)) {
this.log(
`Diff view already exists for ${filePath}, focusing existing view`,
);
// Focus the existing diff view
await this.focusExistingDiff(filePath);
return;
}
// Left side: old content using qwen-diff scheme
const leftDocUri = vscode.Uri.from({
scheme: DIFF_SCHEME,
@@ -103,7 +173,9 @@ export class DiffManager {
this.addDiffDocument(rightDocUri, {
originalFilePath: filePath,
oldContent,
newContent,
leftDocUri,
rightDocUri,
});

View File

@@ -5,7 +5,6 @@
*/
import * as vscode from 'vscode';
// import * as path from 'node:path'; // TODO: 没有生效 - temporarily disabled due to commented out usage
import { IDEServer } from './ide-server.js';
import semver from 'semver';
import { DiffContentProvider, DiffManager } from './diff-manager.js';
@@ -167,46 +166,6 @@ export async function activate(context: vscode.ExtensionContext) {
createWebViewProvider,
);
// TODO: 没有生效
// Relay diff accept/cancel events to the chat webview as assistant notices
// so the user sees immediate feedback in the chat thread (Claude Code style).
// context.subscriptions.push(
// diffManager.onDidChange((notification) => {
// try {
// const method = (notification as { method?: string }).method;
// if (method !== 'ide/diffAccepted' && method !== 'ide/diffClosed') {
// return;
// }
// const params = (
// notification as unknown as {
// params?: { filePath?: string };
// }
// ).params;
// const filePath = params?.filePath ?? '';
// const fileBase = filePath ? path.basename(filePath) : '';
// const text =
// method === 'ide/diffAccepted'
// ? `Accepted changes${fileBase ? ` to ${fileBase}` : ''}.`
// : `Cancelled changes${fileBase ? ` to ${fileBase}` : ''}.`;
// for (const provider of webViewProviders) {
// const panel = provider.getPanel();
// panel?.webview.postMessage({
// type: 'message',
// data: {
// role: 'assistant',
// content: text,
// timestamp: Date.now(),
// },
// });
// }
// } catch (e) {
// console.warn('[Extension] Failed to relay diff event to chat:', e);
// }
// }),
// );
context.subscriptions.push(
vscode.workspace.onDidCloseTextDocument((doc) => {
if (doc.uri.scheme === DIFF_SCHEME) {
@@ -261,34 +220,42 @@ export async function activate(context: vscode.ExtensionContext) {
vscode.workspace.onDidGrantWorkspaceTrust(() => {
ideServer.syncEnvVars();
}),
vscode.commands.registerCommand('qwen-code.runQwenCode', async () => {
const workspaceFolders = vscode.workspace.workspaceFolders;
if (!workspaceFolders || workspaceFolders.length === 0) {
vscode.window.showInformationMessage(
'No folder open. Please open a folder to run Qwen Code.',
);
return;
}
vscode.commands.registerCommand(
'qwen-code.runQwenCode',
async (
location?:
| vscode.TerminalLocation
| vscode.TerminalEditorLocationOptions,
) => {
const workspaceFolders = vscode.workspace.workspaceFolders;
if (!workspaceFolders || workspaceFolders.length === 0) {
vscode.window.showInformationMessage(
'No folder open. Please open a folder to run Qwen Code.',
);
return;
}
let selectedFolder: vscode.WorkspaceFolder | undefined;
if (workspaceFolders.length === 1) {
selectedFolder = workspaceFolders[0];
} else {
selectedFolder = await vscode.window.showWorkspaceFolderPick({
placeHolder: 'Select a folder to run Qwen Code in',
});
}
let selectedFolder: vscode.WorkspaceFolder | undefined;
if (workspaceFolders.length === 1) {
selectedFolder = workspaceFolders[0];
} else {
selectedFolder = await vscode.window.showWorkspaceFolderPick({
placeHolder: 'Select a folder to run Qwen Code in',
});
}
if (selectedFolder) {
const qwenCmd = 'qwen';
const terminal = vscode.window.createTerminal({
name: `Qwen Code (${selectedFolder.name})`,
cwd: selectedFolder.uri.fsPath,
});
terminal.show();
terminal.sendText(qwenCmd);
}
}),
if (selectedFolder) {
const qwenCmd = 'qwen';
const terminal = vscode.window.createTerminal({
name: `Qwen Code (${selectedFolder.name})`,
cwd: selectedFolder.uri.fsPath,
location,
});
terminal.show();
terminal.sendText(qwenCmd);
}
},
),
vscode.commands.registerCommand('qwen-code.showNotices', async () => {
const noticePath = vscode.Uri.joinPath(
context.extensionUri,

View File

@@ -5,6 +5,7 @@
*/
import * as vscode from 'vscode';
import { openChatCommand } from '../commands/index.js';
/**
* Find the editor group immediately to the left of the Qwen chat webview.
@@ -90,7 +91,7 @@ export async function ensureLeftGroupOfChatWebview(): Promise<
// Make the chat group active by revealing the panel
try {
await vscode.commands.executeCommand('qwenCode.openChat');
await vscode.commands.executeCommand(openChatCommand);
} catch {
// Best-effort; continue even if this fails
}
@@ -105,7 +106,7 @@ export async function ensureLeftGroupOfChatWebview(): Promise<
// Restore focus to chat (optional), so we don't disturb user focus
try {
await vscode.commands.executeCommand('qwenCode.openChat');
await vscode.commands.executeCommand(openChatCommand);
} catch {
// Ignore
}

View File

@@ -1,156 +0,0 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import * as vscode from 'vscode';
import { getFileName } from './utils/webviewUtils.js';
/**
* File Operations Handler
* Responsible for handling file opening and diff viewing functionality
*/
export class FileOperations {
/**
* Open file and optionally navigate to specified line and column
* @param filePath File path, can include line and column numbers (format: path/to/file.ts:123 or path/to/file.ts:123:45)
*/
static async openFile(filePath?: string): Promise<void> {
try {
if (!filePath) {
console.warn('[FileOperations] No file path provided');
return;
}
console.log('[FileOperations] Opening file:', filePath);
// Parse file path, line number, and column number
// Formats: path/to/file.ts, path/to/file.ts:123, path/to/file.ts:123:45
const match = filePath.match(/^(.+?)(?::(\d+))?(?::(\d+))?$/);
if (!match) {
console.warn('[FileOperations] Invalid file path format:', filePath);
return;
}
const [, path, lineStr, columnStr] = match;
const lineNumber = lineStr ? parseInt(lineStr, 10) - 1 : 0; // VS Code uses 0-based line numbers
const columnNumber = columnStr ? parseInt(columnStr, 10) - 1 : 0; // VS Code uses 0-based column numbers
// Convert to absolute path if relative
let absolutePath = path;
if (!path.startsWith('/') && !path.match(/^[a-zA-Z]:/)) {
// Relative path - resolve against workspace
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
if (workspaceFolder) {
absolutePath = vscode.Uri.joinPath(workspaceFolder.uri, path).fsPath;
}
}
// Open the document
const uri = vscode.Uri.file(absolutePath);
const document = await vscode.workspace.openTextDocument(uri);
const editor = await vscode.window.showTextDocument(document, {
preview: false,
preserveFocus: false,
});
// Navigate to line and column if specified
if (lineStr) {
const position = new vscode.Position(lineNumber, columnNumber);
editor.selection = new vscode.Selection(position, position);
editor.revealRange(
new vscode.Range(position, position),
vscode.TextEditorRevealType.InCenter,
);
}
console.log('[FileOperations] File opened successfully:', absolutePath);
} catch (error) {
console.error('[FileOperations] Failed to open file:', error);
vscode.window.showErrorMessage(`Failed to open file: ${error}`);
}
}
/**
* Open diff view to compare file changes
* @param data Diff data, including file path, old content, and new content
*/
static async openDiff(data?: {
path?: string;
oldText?: string;
newText?: string;
}): Promise<void> {
try {
if (!data || !data.path) {
console.warn('[FileOperations] No file path provided for diff');
return;
}
const { path, oldText = '', newText = '' } = data;
console.log('[FileOperations] Opening diff for:', path);
// Convert to absolute path if relative
let absolutePath = path;
if (!path.startsWith('/') && !path.match(/^[a-zA-Z]:/)) {
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
if (workspaceFolder) {
absolutePath = vscode.Uri.joinPath(workspaceFolder.uri, path).fsPath;
}
}
// Get the file name for display
const fileName = getFileName(absolutePath);
// Create URIs for old and new content
// Use untitled scheme for old content (before changes)
const oldUri = vscode.Uri.parse(`untitled:${absolutePath}.old`).with({
scheme: 'untitled',
});
// Use the actual file URI for new content
const newUri = vscode.Uri.file(absolutePath);
// Create a TextDocument for the old content using an in-memory document
const _oldDocument = await vscode.workspace.openTextDocument(
oldUri.with({ scheme: 'untitled' }),
);
// Write old content to the document
const edit = new vscode.WorkspaceEdit();
edit.insert(
oldUri.with({ scheme: 'untitled' }),
new vscode.Position(0, 0),
oldText,
);
await vscode.workspace.applyEdit(edit);
// Check if new file exists, if not create it with new content
try {
await vscode.workspace.fs.stat(newUri);
} catch {
// File doesn't exist, create it
const encoder = new TextEncoder();
await vscode.workspace.fs.writeFile(newUri, encoder.encode(newText));
}
// Open diff view
await vscode.commands.executeCommand(
'vscode.diff',
oldUri.with({ scheme: 'untitled' }),
newUri,
`${fileName} (Before ↔ After)`,
{
viewColumn: vscode.ViewColumn.Beside,
preview: false,
preserveFocus: false,
},
);
console.log('[FileOperations] Diff opened successfully');
} catch (error) {
console.error('[FileOperations] Failed to open diff:', error);
vscode.window.showErrorMessage(`Failed to open diff: ${error}`);
}
}
}

View File

@@ -43,34 +43,78 @@ export class PanelManager {
return false; // Panel already exists
}
// We want the chat webview to live in a dedicated, locked group on the
// left. Create a new group on the far left and open the panel there.
try {
// Make sure we start from the first group, then create a group to its left
await vscode.commands.executeCommand(
'workbench.action.focusFirstEditorGroup',
);
await vscode.commands.executeCommand('workbench.action.newGroupLeft');
} catch (error) {
console.warn(
'[PanelManager] Failed to pre-create left editor group (continuing):',
error,
);
}
// First, check if there's an existing Qwen Code group
const existingGroup = this.findExistingQwenCodeGroup();
this.panel = vscode.window.createWebviewPanel(
'qwenCode.chat',
'Qwen Code',
{ viewColumn: vscode.ViewColumn.One, preserveFocus: false }, // Focus and place in leftmost group
{
enableScripts: true,
retainContextWhenHidden: true,
localResourceRoots: [
vscode.Uri.joinPath(this.extensionUri, 'dist'),
vscode.Uri.joinPath(this.extensionUri, 'assets'),
],
},
);
if (existingGroup) {
// If Qwen Code webview already exists in a locked group, create the new panel in that same group
console.log(
'[PanelManager] Found existing Qwen Code group, creating panel in same group',
);
this.panel = vscode.window.createWebviewPanel(
'qwenCode.chat',
'Qwen Code',
{ viewColumn: existingGroup.viewColumn, preserveFocus: false },
{
enableScripts: true,
retainContextWhenHidden: true,
localResourceRoots: [
vscode.Uri.joinPath(this.extensionUri, 'dist'),
vscode.Uri.joinPath(this.extensionUri, 'assets'),
],
},
);
} else {
// If no existing Qwen Code group, create a new group to the right of the active editor group
try {
// Create a new group to the right of the current active group
await vscode.commands.executeCommand('workbench.action.newGroupRight');
} catch (error) {
console.warn(
'[PanelManager] Failed to create right editor group (continuing):',
error,
);
// Fallback: create in current group
const activeColumn =
vscode.window.activeTextEditor?.viewColumn || vscode.ViewColumn.One;
this.panel = vscode.window.createWebviewPanel(
'qwenCode.chat',
'Qwen Code',
{ viewColumn: activeColumn, preserveFocus: false },
{
enableScripts: true,
retainContextWhenHidden: true,
localResourceRoots: [
vscode.Uri.joinPath(this.extensionUri, 'dist'),
vscode.Uri.joinPath(this.extensionUri, 'assets'),
],
},
);
// Lock the group after creation
await this.autoLockEditorGroup();
return true;
}
// Get the new group's view column (should be the active one after creating right)
const newGroupColumn = vscode.window.tabGroups.activeTabGroup.viewColumn;
this.panel = vscode.window.createWebviewPanel(
'qwenCode.chat',
'Qwen Code',
{ viewColumn: newGroupColumn, preserveFocus: false },
{
enableScripts: true,
retainContextWhenHidden: true,
localResourceRoots: [
vscode.Uri.joinPath(this.extensionUri, 'dist'),
vscode.Uri.joinPath(this.extensionUri, 'assets'),
],
},
);
// Lock the group after creation
await this.autoLockEditorGroup();
}
// Set panel icon to Qwen logo
this.panel.iconPath = vscode.Uri.joinPath(

View File

@@ -15,6 +15,7 @@ import { MessageHandler } from '../webview/MessageHandler.js';
import { WebViewContent } from '../webview/WebViewContent.js';
import { CliInstaller } from '../cli/cliInstaller.js';
import { getFileName } from './utils/webviewUtils.js';
import { authMethod } from '../auth/index.js';
export class WebViewProvider {
private panelManager: PanelManager;
@@ -150,8 +151,85 @@ export class WebViewProvider {
type: string;
data: { optionId: string };
}) => {
if (message.type === 'permissionResponse') {
resolve(message.data.optionId);
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);
// 2) If user cancelled/rejected, proactively stop current generation
const isCancel =
optionId === 'cancel' ||
optionId.toLowerCase().includes('reject');
if (isCancel) {
// Fire and forget do not block the ACP resolve
(async () => {
try {
// Stop server-side generation
await this.agentManager.cancelCurrentPrompt();
} catch (err) {
console.warn(
'[WebViewProvider] cancelCurrentPrompt error:',
err,
);
}
// Ensure the webview exits streaming state immediately
this.sendMessageToWebView({
type: 'streamEnd',
data: { timestamp: Date.now(), reason: 'user_cancelled' },
});
// Synthesize a failed tool_call_update to match Claude/CLI UX
try {
const toolCallId =
(request.toolCall as { toolCallId?: string } | undefined)
?.toolCallId || '';
const title =
(request.toolCall as { title?: string } | undefined)
?.title || '';
// Normalize kind for UI fall back to 'execute'
let kind = ((
request.toolCall as { kind?: string } | undefined
)?.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'))
kind = 'edit';
else kind = 'execute';
}
this.sendMessageToWebView({
type: 'toolCall',
data: {
type: 'tool_call_update',
toolCallId,
title,
kind,
status: 'failed',
// Best-effort pass-through (used by UI hints)
rawInput: (request.toolCall as { rawInput?: unknown })
?.rawInput,
locations: (
request.toolCall as {
locations?: Array<{
path: string;
line?: number | null;
}>;
}
)?.locations,
},
});
} catch (err) {
console.warn(
'[WebViewProvider] failed to synthesize failed tool_call_update:',
err,
);
}
})();
}
};
// Store handler in message handler
@@ -339,10 +417,6 @@ export class WebViewProvider {
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
const workingDir = workspaceFolder?.uri.fsPath || process.cwd();
const config = vscode.workspace.getConfiguration('qwenCode');
const openaiApiKey = config.get<string>('qwen.openaiApiKey', '');
const authMethod = openaiApiKey ? 'openai' : 'qwen-oauth';
const hasValidAuth = await this.authStateManager.hasValidAuth(
workingDir,
authMethod,
@@ -392,83 +466,71 @@ export class WebViewProvider {
!!this.authStateManager,
);
const config = vscode.workspace.getConfiguration('qwenCode');
const qwenEnabled = config.get<boolean>('qwen.enabled', true);
// Check if CLI is installed before attempting to connect
const cliDetection = await CliDetector.detectQwenCli();
if (qwenEnabled) {
// Check if CLI is installed before attempting to connect
const cliDetection = await CliDetector.detectQwenCli();
if (!cliDetection.isInstalled) {
console.log(
'[WebViewProvider] Qwen CLI not detected, skipping agent connection',
);
console.log('[WebViewProvider] CLI detection error:', cliDetection.error);
if (!cliDetection.isInstalled) {
console.log(
'[WebViewProvider] Qwen CLI not detected, skipping agent connection',
);
console.log(
'[WebViewProvider] CLI detection error:',
cliDetection.error,
);
// Show VSCode notification with installation option
await CliInstaller.promptInstallation();
// Show VSCode notification with installation option
await CliInstaller.promptInstallation();
// Initialize empty conversation (can still browse history)
await this.initializeEmptyConversation();
} else {
console.log(
'[WebViewProvider] Qwen CLI detected, attempting connection...',
);
console.log('[WebViewProvider] CLI path:', cliDetection.cliPath);
console.log('[WebViewProvider] CLI version:', cliDetection.version);
try {
console.log('[WebViewProvider] Connecting to agent...');
console.log(
'[WebViewProvider] Using authStateManager:',
!!this.authStateManager,
);
const authInfo = await this.authStateManager.getAuthInfo();
console.log('[WebViewProvider] Auth cache status:', authInfo);
// Pass the detected CLI path to ensure we use the correct installation
await this.agentManager.connect(
workingDir,
this.authStateManager,
cliDetection.cliPath,
);
console.log('[WebViewProvider] Agent connected successfully');
this.agentInitialized = true;
// Load messages from the current Qwen session
await this.loadCurrentSessionMessages();
// Notify webview that agent is connected
this.sendMessageToWebView({
type: 'agentConnected',
data: {},
});
} 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.`,
);
// Fallback to empty conversation
await this.initializeEmptyConversation();
// Notify webview that agent connection failed
this.sendMessageToWebView({
type: 'agentConnectionError',
data: {
message: error instanceof Error ? error.message : String(error),
},
});
}
}
} else {
console.log('[WebViewProvider] Qwen agent is disabled in settings');
// Fallback to ConversationStore
// Initialize empty conversation (can still browse history)
await this.initializeEmptyConversation();
} else {
console.log(
'[WebViewProvider] Qwen CLI detected, attempting connection...',
);
console.log('[WebViewProvider] CLI path:', cliDetection.cliPath);
console.log('[WebViewProvider] CLI version:', cliDetection.version);
try {
console.log('[WebViewProvider] Connecting to agent...');
console.log(
'[WebViewProvider] Using authStateManager:',
!!this.authStateManager,
);
const authInfo = await this.authStateManager.getAuthInfo();
console.log('[WebViewProvider] Auth cache status:', authInfo);
// Pass the detected CLI path to ensure we use the correct installation
await this.agentManager.connect(
workingDir,
this.authStateManager,
cliDetection.cliPath,
);
console.log('[WebViewProvider] Agent connected successfully');
this.agentInitialized = true;
// Load messages from the current Qwen session
await this.loadCurrentSessionMessages();
// Notify webview that agent is connected
this.sendMessageToWebView({
type: 'agentConnected',
data: {},
});
} 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.`,
);
// Fallback to empty conversation
await this.initializeEmptyConversation();
// Notify webview that agent connection failed
this.sendMessageToWebView({
type: 'agentConnectionError',
data: {
message: error instanceof Error ? error.message : String(error),
},
});
}
}
}
@@ -614,10 +676,6 @@ export class WebViewProvider {
// First, try to restore an existing session if we have cached auth
if (this.authStateManager) {
const config = vscode.workspace.getConfiguration('qwenCode');
const openaiApiKey = config.get<string>('qwen.openaiApiKey', '');
const authMethod = openaiApiKey ? 'openai' : 'qwen-oauth';
const hasValidAuth = await this.authStateManager.hasValidAuth(
workingDir,
authMethod,
@@ -1001,6 +1059,29 @@ export class WebViewProvider {
*/
async createNewSession(): Promise<void> {
console.log('[WebViewProvider] Creating new session in current panel');
// Check if terminal mode is enabled
const config = vscode.workspace.getConfiguration('qwenCode');
const useTerminal = config.get<boolean>('useTerminal', false);
if (useTerminal) {
// In terminal mode, execute the runQwenCode command to open a new terminal
try {
await vscode.commands.executeCommand('qwen-code.runQwenCode');
console.log('[WebViewProvider] Opened new terminal session');
} catch (error) {
console.error(
'[WebViewProvider] Failed to open new terminal session:',
error,
);
vscode.window.showErrorMessage(
`Failed to open new terminal session: ${error}`,
);
}
return;
}
// WebView mode - create new session via agent manager
try {
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
const workingDir = workspaceFolder?.uri.fsPath || process.cwd();

View File

@@ -10,6 +10,7 @@ import React from 'react';
import type { ToolCallData } from './toolcalls/shared/types.js';
import { FileLink } from './ui/FileLink.js';
import { useVSCode } from '../hooks/useVSCode.js';
import { handleOpenDiff } from '../utils/diffUtils.js';
interface InProgressToolCallProps {
toolCall: ToolCallData;
@@ -138,19 +139,12 @@ export const InProgressToolCall: React.FC<InProgressToolCallProps> = ({
}
// Handle open diff
const handleOpenDiff = () => {
const handleOpenDiffInternal = () => {
if (!diffData) {
return;
}
const path = diffData.path || filePath || '';
vscode.postMessage({
type: 'openDiff',
data: {
path,
oldText: diffData.oldText || '',
newText: diffData.newText || '',
},
});
handleOpenDiff(vscode, path, diffData.oldText, diffData.newText);
};
return (
@@ -179,7 +173,7 @@ export const InProgressToolCall: React.FC<InProgressToolCallProps> = ({
{diffData && (
<button
type="button"
onClick={handleOpenDiff}
onClick={handleOpenDiffInternal}
className="text-[11px] px-2 py-0.5 border border-[var(--app-input-border)] rounded-small text-[var(--app-primary-foreground)] bg-transparent hover:bg-[var(--app-ghost-button-hover-background)] cursor-pointer"
>
Open Diff

View File

@@ -165,8 +165,8 @@
.markdown-content .code-block-wrapper pre {
/* Reserve space so the copy button never overlaps code text */
padding-top: 2rem; /* room for the button height */
padding-right: 2.4rem; /* room for the button width */
padding-top: 1.5rem; /* Reduced padding - room for the button height */
padding-right: 2rem; /* Reduced padding - room for the button width */
}
.markdown-content .code-block-wrapper .copy-button {
@@ -178,21 +178,24 @@
line-height: 1.6;
border-radius: 4px;
border: 1px solid var(--app-primary-border-color);
background-color: var(--app-elevated-background, rgba(255, 255, 255, 0.06));
background-color: var(--app-primary-background, rgba(255, 255, 255, 0.1));
color: var(--app-secondary-foreground);
cursor: pointer;
z-index: 1;
opacity: 0; /* show on hover to reduce visual noise */
transition: opacity 100ms ease-in-out;
pointer-events: none; /* prevent blocking text selection */
}
.markdown-content .code-block-wrapper:hover .copy-button,
.markdown-content .code-block-wrapper .copy-button:focus {
opacity: 1;
pointer-events: auto; /* enable interaction when visible */
}
.markdown-content .code-block-wrapper .copy-button:hover {
background-color: var(--app-list-hover-background, rgba(127, 127, 127, 0.1));
background-color: var(--app-list-hover-background, rgba(127, 127, 127, 0.2));
border-color: var(--app-input-active-border, rgba(97, 95, 255, 0.5));
}
.markdown-content .code-block-wrapper .copy-button:disabled {

View File

@@ -131,9 +131,15 @@ export const PermissionDrawer: React.FC<PermissionDrawerProps> = ({
}
}
// Escape to close
if (e.key === 'Escape' && onClose) {
onClose();
// Escape to cancel permission and close (align with CLI/Claude behavior)
if (e.key === 'Escape') {
e.preventDefault();
const rejectOptionId =
options.find((o) => o.kind.includes('reject'))?.optionId ||
options.find((o) => o.optionId === 'cancel')?.optionId ||
'cancel';
onResponse(rejectOptionId);
if (onClose) onClose();
}
};
@@ -207,10 +213,10 @@ export const PermissionDrawer: React.FC<PermissionDrawerProps> = ({
return (
<button
key={option.optionId}
className={`flex items-center gap-2 px-2 py-1.5 text-left w-full box-border rounded-[4px] border-0 shadow-[inset_0_0_0_1px_var(--app-transparent-inner-border)] transition-colors duration-150 text-[var(--app-primary-foreground)] hover:bg-[var(--app-input-background)] ${
className={`flex items-center gap-2 px-2 py-1.5 text-left w-full box-border rounded-[4px] border-0 shadow-[inset_0_0_0_1px_var(--app-transparent-inner-border)] transition-colors duration-150 text-[var(--app-primary-foreground)] hover:bg-[var(--app-list-hover-background)] ${
isFocused
? 'text-[var(--app-list-active-foreground)] hover:text-[var(--app-button-foreground)] hover:font-bold hover:relative hover:border-0'
: 'hover:text-[var(--app-button-foreground)] hover:font-bold hover:relative hover:border-0'
? 'text-[var(--app-list-active-foreground)] bg-[var(--app-list-active-background)] hover:text-[var(--app-button-foreground)] hover:font-bold hover:relative hover:border-0'
: 'hover:bg-[var(--app-button-background)] hover:text-[var(--app-button-foreground)] hover:font-bold hover:relative hover:border-0'
}`}
onClick={() => onResponse(option.optionId)}
onMouseEnter={() => setFocusedIndex(index)}

View File

@@ -1,65 +0,0 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*
* Playback and session control icons
*/
import type React from 'react';
import type { IconProps } from './types.js';
/**
* Play/resume icon (16x16)
* Used for resume session
*/
export const PlayIcon: React.FC<IconProps> = ({
size = 16,
className,
...props
}) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
width={size}
height={size}
className={className}
aria-hidden="true"
{...props}
>
<path d="M5.33337 4L10.6667 8L5.33337 12" />
</svg>
);
/**
* Switch/arrow right icon (16x16)
* Used for switch session
*/
export const SwitchIcon: React.FC<IconProps> = ({
size = 16,
className,
...props
}) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
width={size}
height={size}
className={className}
aria-hidden="true"
{...props}
>
<path d="M10.6666 4L13.3333 6.66667L10.6666 9.33333" />
<path d="M2.66663 6.66667H13.3333" />
</svg>
);

View File

@@ -2,14 +2,9 @@
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*
* Icons index - exports all icon components
*/
// Types
export type { IconProps } from './types.js';
// File icons
export { FileIcon, FileListIcon, FolderIcon } from './FileIcons.js';
// Navigation icons
@@ -47,9 +42,6 @@ export {
SelectionIcon,
} from './StatusIcons.js';
// Action icons
export { PlayIcon, SwitchIcon } from './ActionIcons.js';
// Special icons
export { ThinkingIcon, TerminalIcon } from './SpecialIcons.js';

View File

@@ -15,7 +15,7 @@ import {
} from '../shared/utils.js';
import { useVSCode } from '../../../hooks/useVSCode.js';
import { FileLink } from '../../ui/FileLink.js';
import { isDevelopmentMode } from '../../../utils/envUtils.js';
import { handleOpenDiff } from '../../../utils/diffUtils.js';
/**
* Calculate diff summary (added/removed lines)
@@ -47,26 +47,13 @@ export const EditToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
// Group content by type; memoize to avoid new array identities on every render
const { errors, diffs } = useMemo(() => groupContent(content), [content]);
// TODO:
// console.log('EditToolCall', {
// content,
// locations,
// toolCallId,
// errors,
// diffs,
// });
const handleOpenDiff = useCallback(
const handleOpenDiffInternal = useCallback(
(
path: string | undefined,
oldText: string | null | undefined,
newText: string | undefined,
) => {
if (path) {
vscode.postMessage({
type: 'openDiff',
data: { path, oldText: oldText || '', newText: newText || '' },
});
}
handleOpenDiff(vscode, path, oldText, newText);
},
[vscode],
);
@@ -74,24 +61,9 @@ export const EditToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
// Extract filename from path
const getFileName = (path: string): string => path.split('/').pop() || path;
// Keep a module-scoped set to ensure auto-open fires once per toolCallId across re-renders
// const autoOpenedToolCallIds =
// (
// globalThis as unknown as {
// __qwenAutoOpenedDiffIds?: Set<string>;
// }
// ).__qwenAutoOpenedDiffIds || new Set<string>();
// (
// globalThis as unknown as { __qwenAutoOpenedDiffIds: Set<string> }
// ).__qwenAutoOpenedDiffIds = autoOpenedToolCallIds;
// Automatically trigger openDiff when diff content is detected (Claude Code style)
// Automatically trigger openDiff when diff content is detected
// Only trigger once per tool call by checking toolCallId
useEffect(() => {
// Guard: already auto-opened for this toolCallId in this webview session
// if (autoOpenedToolCallIds.has(toolCallId)) {
// return;
// }
// Only auto-open if there are diffs and we have the required data
if (diffs.length > 0) {
const firstDiff = diffs[0];
@@ -104,8 +76,7 @@ export const EditToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
) {
// Add a small delay to ensure the component is fully rendered
const timer = setTimeout(() => {
handleOpenDiff(path, firstDiff.oldText, firstDiff.newText);
// autoOpenedToolCallIds.add(toolCallId);
handleOpenDiffInternal(path, firstDiff.oldText, firstDiff.newText);
}, 100);
// Proper cleanup function
return () => timer && clearTimeout(timer);
@@ -142,17 +113,11 @@ export const EditToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
if (diffs.length > 0) {
const firstDiff = diffs[0];
const path = firstDiff.path || (locations && locations[0]?.path) || '';
// const fileName = path ? getFileName(path) : '';
const summary = getDiffSummary(firstDiff.oldText, firstDiff.newText);
// No hooks here; define a simple click handler scoped to this block
// const openFirstDiff = () =>
// handleOpenDiff(path, firstDiff.oldText, firstDiff.newText);
const containerStatus = mapToolStatusToContainerStatus(toolCall.status);
return (
<div
className={`qwen-message message-item relative py-2 select-text cursor-pointer hover:bg-[var(--app-input-background)] toolcall-container toolcall-status-${containerStatus}`}
// onClick={openFirstDiff}
className={`qwen-message message-item relative py-2 select-text toolcall-container toolcall-status-${containerStatus}`}
title="Open diff in VS Code"
>
{/* IMPORTANT: Always include min-w-0/max-w-full on inner wrappers to prevent overflow. */}
@@ -176,13 +141,6 @@ export const EditToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
<span className="flex-shrink-0 relative top-[-0.1em]"></span>
<span className="flex-shrink-0 w-full">{summary}</span>
</div>
{/* Show toolCallId only in development/debug mode */}
{toolCallId && isDevelopmentMode() && (
<span className="text-[10px] opacity-30">
[{toolCallId.slice(-8)}]
</span>
)}
</div>
</div>
);

View File

@@ -17,6 +17,7 @@ import {
import { DiffDisplay } from './shared/DiffDisplay.js';
import { safeTitle, groupContent } from './shared/utils.js';
import { useVSCode } from '../../hooks/useVSCode.js';
import { handleOpenDiff } from '../../utils/diffUtils.js';
/**
* Generic tool call component that can display any tool call type
@@ -31,19 +32,6 @@ export const GenericToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
// Group content by type
const { textOutputs, errors, diffs } = groupContent(content);
const handleOpenDiff = (
path: string | undefined,
oldText: string | null | undefined,
newText: string | undefined,
) => {
if (path) {
vscode.postMessage({
type: 'openDiff',
data: { path, oldText: oldText || '', newText: newText || '' },
});
}
};
// Error case: show operation + error in card layout
if (errors.length > 0) {
return (
@@ -70,7 +58,7 @@ export const GenericToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
oldText={item.oldText}
newText={item.newText}
onOpenDiff={() =>
handleOpenDiff(item.path, item.oldText, item.newText)
handleOpenDiff(vscode, item.path, item.oldText, item.newText)
}
/>
</div>

View File

@@ -16,6 +16,7 @@ import {
} from '../shared/utils.js';
import { FileLink } from '../../ui/FileLink.js';
import { useVSCode } from '../../../hooks/useVSCode.js';
import { handleOpenDiff } from '../../../utils/diffUtils.js';
/**
* Specialized component for Read tool calls
@@ -30,18 +31,13 @@ export const ReadToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
const { errors, diffs } = useMemo(() => groupContent(content), [content]);
// Post a message to the extension host to open a VS Code diff tab
const handleOpenDiff = useCallback(
const handleOpenDiffInternal = useCallback(
(
path: string | undefined,
oldText: string | null | undefined,
newText: string | undefined,
) => {
if (path) {
vscode.postMessage({
type: 'openDiff',
data: { path, oldText: oldText || '', newText: newText || '' },
});
}
handleOpenDiff(vscode, path, oldText, newText);
},
[vscode],
);
@@ -59,7 +55,7 @@ export const ReadToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
firstDiff.newText !== undefined
) {
const timer = setTimeout(() => {
handleOpenDiff(path, firstDiff.oldText, firstDiff.newText);
handleOpenDiffInternal(path, firstDiff.oldText, firstDiff.newText);
}, 100);
return () => timer && clearTimeout(timer);
}

View File

@@ -9,7 +9,6 @@
import type React from 'react';
import { FileLink } from '../../ui/FileLink.js';
import { isDevelopmentMode } from '../../../utils/envUtils.js';
import './LayoutComponents.css';
/**
@@ -64,13 +63,6 @@ export const ToolCallContainer: React.FC<ToolCallContainerProps> = ({
{children}
</div>
)}
{/* Show toolCallId only in development/debug mode */}
{_toolCallId && isDevelopmentMode() && (
<span className="text-[10px] opacity-30">
[{_toolCallId.slice(-8)}]
</span>
)}
</div>
</div>
);

View File

@@ -9,8 +9,8 @@ import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { BaseMessageHandler } from './BaseMessageHandler.js';
import { FileOperations } from '../FileOperations.js';
import { getFileName } from '../utils/webviewUtils.js';
import { showDiffCommand } from '../../commands/index.js';
/**
* File message handler
@@ -316,14 +316,56 @@ export class FileMessageHandler extends BaseMessageHandler {
/**
* Open file
*/
private async handleOpenFile(path?: string): Promise<void> {
if (!path) {
private async handleOpenFile(filePath?: string): Promise<void> {
if (!filePath) {
console.warn('[FileMessageHandler] No path provided for openFile');
return;
}
try {
await FileOperations.openFile(path);
console.log('[FileOperations] Opening file:', filePath);
// Parse file path, line number, and column number
// Formats: path/to/file.ts, path/to/file.ts:123, path/to/file.ts:123:45
const match = filePath.match(/^(.+?)(?::(\d+))?(?::(\d+))?$/);
if (!match) {
console.warn('[FileOperations] Invalid file path format:', filePath);
return;
}
const [, path, lineStr, columnStr] = match;
const lineNumber = lineStr ? parseInt(lineStr, 10) - 1 : 0; // VS Code uses 0-based line numbers
const columnNumber = columnStr ? parseInt(columnStr, 10) - 1 : 0; // VS Code uses 0-based column numbers
// Convert to absolute path if relative
let absolutePath = path;
if (!path.startsWith('/') && !path.match(/^[a-zA-Z]:/)) {
// Relative path - resolve against workspace
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
if (workspaceFolder) {
absolutePath = vscode.Uri.joinPath(workspaceFolder.uri, path).fsPath;
}
}
// Open the document
const uri = vscode.Uri.file(absolutePath);
const document = await vscode.workspace.openTextDocument(uri);
const editor = await vscode.window.showTextDocument(document, {
preview: false,
preserveFocus: false,
});
// Navigate to line and column if specified
if (lineStr) {
const position = new vscode.Position(lineNumber, columnNumber);
editor.selection = new vscode.Selection(position, position);
editor.revealRange(
new vscode.Range(position, position),
vscode.TextEditorRevealType.InCenter,
);
}
console.log('[FileOperations] File opened successfully:', absolutePath);
} catch (error) {
console.error('[FileMessageHandler] Failed to open file:', error);
vscode.window.showErrorMessage(`Failed to open file: ${error}`);
@@ -342,7 +384,7 @@ export class FileMessageHandler extends BaseMessageHandler {
}
try {
await vscode.commands.executeCommand('qwenCode.showDiff', {
await vscode.commands.executeCommand(showDiffCommand, {
path: (data.path as string) || '',
oldText: (data.oldText as string) || '',
newText: (data.newText as string) || '',

View File

@@ -0,0 +1,30 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*
* Shared utilities for handling diff operations in the webview
*/
import type { WebviewApi } from 'vscode-webview';
/**
* Handle opening a diff view for a file
* @param vscode Webview API instance
* @param path File path
* @param oldText Original content (left side)
* @param newText New content (right side)
*/
export const handleOpenDiff = (
vscode: WebviewApi<unknown>,
path: string | undefined,
oldText: string | null | undefined,
newText: string | undefined,
): void => {
if (path) {
vscode.postMessage({
type: 'openDiff',
data: { path, oldText: oldText || '', newText: newText || '' },
});
}
};

View File

@@ -1,15 +0,0 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
export function isDevelopmentMode(): boolean {
// TODO: 调试用
// return false;
return (
process.env.NODE_ENV === 'development' ||
process.env.DEBUG === 'true' ||
process.env.NODE_ENV !== 'production'
);
}

View File

@@ -9,17 +9,17 @@
export default {
content: [
// Progressive adoption strategy: Only scan newly created Tailwind components
'./src/webview/App.tsx',
'./src/webview/components/ui/**/*.{js,jsx,ts,tsx}',
'./src/webview/components/messages/**/*.{js,jsx,ts,tsx}',
'./src/webview/components/toolcalls/**/*.{js,jsx,ts,tsx}',
'./src/webview/components/InProgressToolCall.tsx',
'./src/webview/components/MessageContent.tsx',
'./src/webview/components/InputForm.tsx',
'./src/webview/components/PermissionDrawer.tsx',
'./src/webview/components/PlanDisplay.tsx',
'./src/webview/components/session/SessionSelector.tsx',
'./src/webview/components/messages/UserMessage.tsx',
// './src/webview/App.tsx',
'./src/webview/**/*.{js,jsx,ts,tsx}',
// './src/webview/components/messages/**/*.{js,jsx,ts,tsx}',
// './src/webview/components/toolcalls/**/*.{js,jsx,ts,tsx}',
// './src/webview/components/InProgressToolCall.tsx',
// './src/webview/components/MessageContent.tsx',
// './src/webview/components/InputForm.tsx',
// './src/webview/components/PermissionDrawer.tsx',
// './src/webview/components/PlanDisplay.tsx',
// './src/webview/components/session/SessionSelector.tsx',
// './src/webview/components/messages/UserMessage.tsx',
],
theme: {
extend: {