mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
fix(vscode-ide-companion): fix bugs & support terminal mode operation
This commit is contained in:
@@ -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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
7
packages/vscode-ide-companion/src/auth/index.ts
Normal file
7
packages/vscode-ide-companion/src/auth/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
export const authMethod = 'qwen-oauth';
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
vscode.commands.registerCommand('qwenCode.openNewChatTab', async () => {
|
||||
const provider = createWebViewProvider();
|
||||
provider.show();
|
||||
}
|
||||
await provider.show();
|
||||
}),
|
||||
);
|
||||
|
||||
// qwenCode.openNewChatTab (not contributed in package.json; used programmatically)
|
||||
disposables.push(
|
||||
vscode.commands.registerCommand('qwenCode.openNewChatTab', () => {
|
||||
const provider = createWebViewProvider();
|
||||
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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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,7 +220,13 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
vscode.workspace.onDidGrantWorkspaceTrust(() => {
|
||||
ideServer.syncEnvVars();
|
||||
}),
|
||||
vscode.commands.registerCommand('qwen-code.runQwenCode', async () => {
|
||||
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(
|
||||
@@ -284,11 +249,13 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,25 +43,18 @@ 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();
|
||||
|
||||
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: vscode.ViewColumn.One, preserveFocus: false }, // Focus and place in leftmost group
|
||||
{ viewColumn: existingGroup.viewColumn, preserveFocus: false },
|
||||
{
|
||||
enableScripts: true,
|
||||
retainContextWhenHidden: true,
|
||||
@@ -71,6 +64,57 @@ export class PanelManager {
|
||||
],
|
||||
},
|
||||
);
|
||||
} 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(
|
||||
|
||||
@@ -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,10 +466,6 @@ export class WebViewProvider {
|
||||
!!this.authStateManager,
|
||||
);
|
||||
|
||||
const config = vscode.workspace.getConfiguration('qwenCode');
|
||||
const qwenEnabled = config.get<boolean>('qwen.enabled', true);
|
||||
|
||||
if (qwenEnabled) {
|
||||
// Check if CLI is installed before attempting to connect
|
||||
const cliDetection = await CliDetector.detectQwenCli();
|
||||
|
||||
@@ -403,10 +473,7 @@ export class WebViewProvider {
|
||||
console.log(
|
||||
'[WebViewProvider] Qwen CLI not detected, skipping agent connection',
|
||||
);
|
||||
console.log(
|
||||
'[WebViewProvider] CLI detection error:',
|
||||
cliDetection.error,
|
||||
);
|
||||
console.log('[WebViewProvider] CLI detection error:', cliDetection.error);
|
||||
|
||||
// Show VSCode notification with installation option
|
||||
await CliInstaller.promptInstallation();
|
||||
@@ -465,11 +532,6 @@ export class WebViewProvider {
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log('[WebViewProvider] Qwen agent is disabled in settings');
|
||||
// Fallback to ConversationStore
|
||||
await this.initializeEmptyConversation();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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) || '',
|
||||
|
||||
30
packages/vscode-ide-companion/src/webview/utils/diffUtils.ts
Normal file
30
packages/vscode-ide-companion/src/webview/utils/diffUtils.ts
Normal 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 || '' },
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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'
|
||||
);
|
||||
}
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user