From 96b275a7566518507a692f33f364e5756ae6bf6c Mon Sep 17 00:00:00 2001 From: yiliang114 <1204183885@qq.com> Date: Sat, 6 Dec 2025 00:30:22 +0800 Subject: [PATCH] fix(vscode-ide-companion): fix bugs & support terminal mode operation --- packages/vscode-ide-companion/package.json | 31 +-- .../src/acp/acpSessionManager.ts | 4 +- .../src/agents/qwenAgentManager.ts | 5 +- .../src/agents/qwenConnectionHandler.ts | 29 +- .../vscode-ide-companion/src/auth/index.ts | 7 + .../src/cli/cliVersionManager.ts | 2 +- .../src/commands/index.ts | 51 ++-- .../src/constants/acpSchema.ts | 8 - .../vscode-ide-companion/src/diff-manager.ts | 72 +++++ .../vscode-ide-companion/src/extension.ts | 101 +++---- .../src/utils/editorGroupUtils.ts | 5 +- .../src/webview/FileOperations.ts | 156 ----------- .../src/webview/PanelManager.ts | 98 +++++-- .../src/webview/WebViewProvider.ts | 247 ++++++++++++------ .../webview/components/InProgressToolCall.tsx | 14 +- .../MarkdownRenderer/MarkdownRenderer.css | 11 +- .../webview/components/PermissionDrawer.tsx | 18 +- .../webview/components/icons/ActionIcons.tsx | 65 ----- .../src/webview/components/icons/index.ts | 8 - .../toolcalls/Edit/EditToolCall.tsx | 54 +--- .../components/toolcalls/GenericToolCall.tsx | 16 +- .../toolcalls/Read/ReadToolCall.tsx | 12 +- .../toolcalls/shared/LayoutComponents.tsx | 8 - .../webview/handlers/FileMessageHandler.ts | 52 +++- .../src/webview/utils/diffUtils.ts | 30 +++ .../src/webview/utils/envUtils.ts | 15 -- .../vscode-ide-companion/tailwind.config.js | 22 +- 27 files changed, 515 insertions(+), 626 deletions(-) create mode 100644 packages/vscode-ide-companion/src/auth/index.ts delete mode 100644 packages/vscode-ide-companion/src/webview/FileOperations.ts delete mode 100644 packages/vscode-ide-companion/src/webview/components/icons/ActionIcons.tsx create mode 100644 packages/vscode-ide-companion/src/webview/utils/diffUtils.ts delete mode 100644 packages/vscode-ide-companion/src/webview/utils/envUtils.ts diff --git a/packages/vscode-ide-companion/package.json b/packages/vscode-ide-companion/package.json index 8865ec2a..fc9d7836 100644 --- a/packages/vscode-ide-companion/package.json +++ b/packages/vscode-ide-companion/package.json @@ -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" } } }, diff --git a/packages/vscode-ide-companion/src/acp/acpSessionManager.ts b/packages/vscode-ide-companion/src/acp/acpSessionManager.ts index 2f9b2a02..efe82331 100644 --- a/packages/vscode-ide-companion/src/acp/acpSessionManager.ts +++ b/packages/vscode-ide-companion/src/acp/acpSessionManager.ts @@ -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( - CUSTOM_METHODS.session_list, + AGENT_METHODS.session_list, {}, child, pendingRequests, diff --git a/packages/vscode-ide-companion/src/agents/qwenAgentManager.ts b/packages/vscode-ide-companion/src/agents/qwenAgentManager.ts index 48f02915..bed783a8 100644 --- a/packages/vscode-ide-companion/src/agents/qwenAgentManager.ts +++ b/packages/vscode-ide-companion/src/agents/qwenAgentManager.ts @@ -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('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. diff --git a/packages/vscode-ide-companion/src/agents/qwenConnectionHandler.ts b/packages/vscode-ide-companion/src/agents/qwenConnectionHandler.ts index 23c77875..5e372ab8 100644 --- a/packages/vscode-ide-companion/src/agents/qwenConnectionHandler.ts +++ b/packages/vscode-ide-companion/src/agents/qwenConnectionHandler.ts @@ -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('qwen.cliPath', 'qwen'); - const openaiApiKey = config.get('qwen.openaiApiKey', ''); - const openaiBaseUrl = config.get('qwen.openaiBaseUrl', ''); - const model = config.get('qwen.model', ''); - const proxy = config.get('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'); } diff --git a/packages/vscode-ide-companion/src/auth/index.ts b/packages/vscode-ide-companion/src/auth/index.ts new file mode 100644 index 00000000..076e94cb --- /dev/null +++ b/packages/vscode-ide-companion/src/auth/index.ts @@ -0,0 +1,7 @@ +/** + * @license + * Copyright 2025 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + */ + +export const authMethod = 'qwen-oauth'; diff --git a/packages/vscode-ide-companion/src/cli/cliVersionManager.ts b/packages/vscode-ide-companion/src/cli/cliVersionManager.ts index 2caa8a9b..acf76bf6 100644 --- a/packages/vscode-ide-companion/src/cli/cliVersionManager.ts +++ b/packages/vscode-ide-companion/src/cli/cliVersionManager.ts @@ -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 diff --git a/packages/vscode-ide-companion/src/commands/index.ts b/packages/vscode-ide-companion/src/commands/index.ts index 92c188bd..ef432825 100644 --- a/packages/vscode-ide-companion/src/commands/index.ts +++ b/packages/vscode-ide-companion/src/commands/index.ts @@ -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('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(); diff --git a/packages/vscode-ide-companion/src/constants/acpSchema.ts b/packages/vscode-ide-companion/src/constants/acpSchema.ts index 43d72415..5f826f97 100644 --- a/packages/vscode-ide-companion/src/constants/acpSchema.ts +++ b/packages/vscode-ide-companion/src/constants/acpSchema.ts @@ -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; diff --git a/packages/vscode-ide-companion/src/diff-manager.ts b/packages/vscode-ide-companion/src/diff-manager.ts index cca203ea..ebaf8d7d 100644 --- a/packages/vscode-ide-companion/src/diff-manager.ts +++ b/packages/vscode-ide-companion/src/diff-manager.ts @@ -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 { + 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, }); diff --git a/packages/vscode-ide-companion/src/extension.ts b/packages/vscode-ide-companion/src/extension.ts index 69dd2c09..f4d882b3 100644 --- a/packages/vscode-ide-companion/src/extension.ts +++ b/packages/vscode-ide-companion/src/extension.ts @@ -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, diff --git a/packages/vscode-ide-companion/src/utils/editorGroupUtils.ts b/packages/vscode-ide-companion/src/utils/editorGroupUtils.ts index c4827b35..3bfc675f 100644 --- a/packages/vscode-ide-companion/src/utils/editorGroupUtils.ts +++ b/packages/vscode-ide-companion/src/utils/editorGroupUtils.ts @@ -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 } diff --git a/packages/vscode-ide-companion/src/webview/FileOperations.ts b/packages/vscode-ide-companion/src/webview/FileOperations.ts deleted file mode 100644 index 79032f0c..00000000 --- a/packages/vscode-ide-companion/src/webview/FileOperations.ts +++ /dev/null @@ -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 { - 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 { - 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}`); - } - } -} diff --git a/packages/vscode-ide-companion/src/webview/PanelManager.ts b/packages/vscode-ide-companion/src/webview/PanelManager.ts index 5244441a..283276a8 100644 --- a/packages/vscode-ide-companion/src/webview/PanelManager.ts +++ b/packages/vscode-ide-companion/src/webview/PanelManager.ts @@ -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( diff --git a/packages/vscode-ide-companion/src/webview/WebViewProvider.ts b/packages/vscode-ide-companion/src/webview/WebViewProvider.ts index 1efde30c..5d97ac31 100644 --- a/packages/vscode-ide-companion/src/webview/WebViewProvider.ts +++ b/packages/vscode-ide-companion/src/webview/WebViewProvider.ts @@ -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('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('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('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 { 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('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(); diff --git a/packages/vscode-ide-companion/src/webview/components/InProgressToolCall.tsx b/packages/vscode-ide-companion/src/webview/components/InProgressToolCall.tsx index 67e13777..b0837fb1 100644 --- a/packages/vscode-ide-companion/src/webview/components/InProgressToolCall.tsx +++ b/packages/vscode-ide-companion/src/webview/components/InProgressToolCall.tsx @@ -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 = ({ } // 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 = ({ {diffData && (