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": {
|
"configuration": {
|
||||||
"title": "Qwen Code",
|
"title": "Qwen Code",
|
||||||
"properties": {
|
"properties": {
|
||||||
"qwenCode.qwen.enabled": {
|
"qwenCode.useTerminal": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"default": true,
|
"default": "false",
|
||||||
"description": "Enable Qwen agent integration"
|
"description": "Use terminal to run Qwen Code"
|
||||||
},
|
|
||||||
"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)"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import type {
|
|||||||
AcpNotification,
|
AcpNotification,
|
||||||
AcpResponse,
|
AcpResponse,
|
||||||
} from '../constants/acpTypes.js';
|
} 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 { PendingRequest } from './connectionTypes.js';
|
||||||
import type { ChildProcess } from 'child_process';
|
import type { ChildProcess } from 'child_process';
|
||||||
|
|
||||||
@@ -306,7 +306,7 @@ export class AcpSessionManager {
|
|||||||
console.log('[ACP] Requesting session list...');
|
console.log('[ACP] Requesting session list...');
|
||||||
try {
|
try {
|
||||||
const response = await this.sendRequest<AcpResponse>(
|
const response = await this.sendRequest<AcpResponse>(
|
||||||
CUSTOM_METHODS.session_list,
|
AGENT_METHODS.session_list,
|
||||||
{},
|
{},
|
||||||
child,
|
child,
|
||||||
pendingRequests,
|
pendingRequests,
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
* Copyright 2025 Qwen Team
|
* Copyright 2025 Qwen Team
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
import * as vscode from 'vscode';
|
|
||||||
import { AcpConnection } from '../acp/acpConnection.js';
|
import { AcpConnection } from '../acp/acpConnection.js';
|
||||||
import type {
|
import type {
|
||||||
AcpSessionUpdate,
|
AcpSessionUpdate,
|
||||||
@@ -24,6 +23,7 @@ import type {
|
|||||||
import { QwenConnectionHandler } from './qwenConnectionHandler.js';
|
import { QwenConnectionHandler } from './qwenConnectionHandler.js';
|
||||||
import { QwenSessionUpdateHandler } from './qwenSessionUpdateHandler.js';
|
import { QwenSessionUpdateHandler } from './qwenSessionUpdateHandler.js';
|
||||||
import { CliContextManager } from '../cli/cliContextManager.js';
|
import { CliContextManager } from '../cli/cliContextManager.js';
|
||||||
|
import { authMethod } from '../auth/index.js';
|
||||||
|
|
||||||
export type { ChatMessage, PlanEntry, ToolCallUpdateData };
|
export type { ChatMessage, PlanEntry, ToolCallUpdateData };
|
||||||
|
|
||||||
@@ -682,9 +682,6 @@ export class QwenAgentManager {
|
|||||||
|
|
||||||
// Check if we have valid cached authentication
|
// Check if we have valid cached authentication
|
||||||
let hasValidAuth = false;
|
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
|
// Prefer the provided authStateManager, otherwise fall back to the one
|
||||||
// remembered during connect(). This prevents accidental re-auth in
|
// remembered during connect(). This prevents accidental re-auth in
|
||||||
// fallback paths (e.g. session switching) when the handler didn't pass it.
|
// 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 type { AuthStateManager } from '../auth/authStateManager.js';
|
||||||
import { CliVersionManager } from '../cli/cliVersionManager.js';
|
import { CliVersionManager } from '../cli/cliVersionManager.js';
|
||||||
import { CliContextManager } from '../cli/cliContextManager.js';
|
import { CliContextManager } from '../cli/cliContextManager.js';
|
||||||
|
import { authMethod } from '../auth/index.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Qwen Connection Handler class
|
* Qwen Connection Handler class
|
||||||
@@ -66,47 +67,23 @@ export class QwenConnectionHandler {
|
|||||||
// Use the provided CLI path if available, otherwise use the configured path
|
// Use the provided CLI path if available, otherwise use the configured path
|
||||||
const effectiveCliPath =
|
const effectiveCliPath =
|
||||||
cliPath || config.get<string>('qwen.cliPath', 'qwen');
|
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[] = [];
|
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);
|
await connection.connect('qwen', effectiveCliPath, workingDir, extraArgs);
|
||||||
|
|
||||||
// Determine authentication method
|
|
||||||
const authMethod = openaiApiKey ? 'openai' : 'qwen-oauth';
|
|
||||||
|
|
||||||
// Check if we have valid cached authentication
|
// Check if we have valid cached authentication
|
||||||
if (authStateManager) {
|
if (authStateManager) {
|
||||||
console.log('[QwenAgentManager] Checking for cached authentication...');
|
console.log('[QwenAgentManager] Checking for cached authentication...');
|
||||||
console.log('[QwenAgentManager] Working dir:', workingDir);
|
console.log('[QwenAgentManager] Working dir:', workingDir);
|
||||||
console.log('[QwenAgentManager] Auth method:', authMethod);
|
console.log('[QwenAgentManager] Auth method:', authMethod);
|
||||||
|
|
||||||
const hasValidAuth = await authStateManager.hasValidAuth(
|
const hasValidAuth = await authStateManager.hasValidAuth(
|
||||||
workingDir,
|
workingDir,
|
||||||
authMethod,
|
authMethod,
|
||||||
);
|
);
|
||||||
console.log('[QwenAgentManager] Has valid auth:', hasValidAuth);
|
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 {
|
} else {
|
||||||
console.log('[QwenAgentManager] No authStateManager provided');
|
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
|
* 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
|
* CLI Feature Flags based on version
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ import type { WebViewProvider } from '../webview/WebViewProvider.js';
|
|||||||
|
|
||||||
type Logger = (message: string) => void;
|
type Logger = (message: string) => void;
|
||||||
|
|
||||||
|
export const showDiffCommand = 'qwenCode.showDiff';
|
||||||
|
export const openChatCommand = 'qwenCode.openChat';
|
||||||
|
|
||||||
export function registerNewCommands(
|
export function registerNewCommands(
|
||||||
context: vscode.ExtensionContext,
|
context: vscode.ExtensionContext,
|
||||||
log: Logger,
|
log: Logger,
|
||||||
@@ -13,10 +16,33 @@ export function registerNewCommands(
|
|||||||
): void {
|
): void {
|
||||||
const disposables: vscode.Disposable[] = [];
|
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(
|
disposables.push(
|
||||||
vscode.commands.registerCommand(
|
vscode.commands.registerCommand(
|
||||||
'qwenCode.showDiff',
|
showDiffCommand,
|
||||||
async (args: { path: string; oldText: string; newText: string }) => {
|
async (args: { path: string; oldText: string; newText: string }) => {
|
||||||
log(`[Command] showDiff called for: ${args.path}`);
|
log(`[Command] showDiff called for: ${args.path}`);
|
||||||
try {
|
try {
|
||||||
@@ -40,28 +66,14 @@ export function registerNewCommands(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// qwenCode.openChat
|
// TODO: qwenCode.openNewChatTab (not contributed in package.json; used programmatically)
|
||||||
disposables.push(
|
disposables.push(
|
||||||
vscode.commands.registerCommand('qwenCode.openChat', () => {
|
vscode.commands.registerCommand('qwenCode.openNewChatTab', async () => {
|
||||||
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', () => {
|
|
||||||
const provider = createWebViewProvider();
|
const provider = createWebViewProvider();
|
||||||
provider.show();
|
await provider.show();
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
// qwenCode.clearAuthCache
|
|
||||||
disposables.push(
|
disposables.push(
|
||||||
vscode.commands.registerCommand('qwenCode.clearAuthCache', async () => {
|
vscode.commands.registerCommand('qwenCode.clearAuthCache', async () => {
|
||||||
const providers = getWebViewProviders();
|
const providers = getWebViewProviders();
|
||||||
@@ -75,7 +87,6 @@ export function registerNewCommands(
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
// qwenCode.login
|
|
||||||
disposables.push(
|
disposables.push(
|
||||||
vscode.commands.registerCommand('qwenCode.login', async () => {
|
vscode.commands.registerCommand('qwenCode.login', async () => {
|
||||||
const providers = getWebViewProviders();
|
const providers = getWebViewProviders();
|
||||||
|
|||||||
@@ -50,11 +50,3 @@ export const CLIENT_METHODS = {
|
|||||||
session_request_permission: 'session/request_permission',
|
session_request_permission: 'session/request_permission',
|
||||||
session_update: 'session/update',
|
session_update: 'session/update',
|
||||||
} as const;
|
} 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.
|
// Information about a diff view that is currently open.
|
||||||
interface DiffInfo {
|
interface DiffInfo {
|
||||||
originalFilePath: string;
|
originalFilePath: string;
|
||||||
|
oldContent: string;
|
||||||
newContent: string;
|
newContent: string;
|
||||||
|
leftDocUri: vscode.Uri;
|
||||||
rightDocUri: 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.
|
* Creates and shows a new diff view.
|
||||||
* @param filePath Path to the file being diffed
|
* @param filePath Path to the file being diffed
|
||||||
@@ -85,6 +146,15 @@ export class DiffManager {
|
|||||||
* @param newContent The modified content (right side)
|
* @param newContent The modified content (right side)
|
||||||
*/
|
*/
|
||||||
async showDiff(filePath: string, oldContent: string, newContent: string) {
|
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
|
// Left side: old content using qwen-diff scheme
|
||||||
const leftDocUri = vscode.Uri.from({
|
const leftDocUri = vscode.Uri.from({
|
||||||
scheme: DIFF_SCHEME,
|
scheme: DIFF_SCHEME,
|
||||||
@@ -103,7 +173,9 @@ export class DiffManager {
|
|||||||
|
|
||||||
this.addDiffDocument(rightDocUri, {
|
this.addDiffDocument(rightDocUri, {
|
||||||
originalFilePath: filePath,
|
originalFilePath: filePath,
|
||||||
|
oldContent,
|
||||||
newContent,
|
newContent,
|
||||||
|
leftDocUri,
|
||||||
rightDocUri,
|
rightDocUri,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as vscode from 'vscode';
|
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 { IDEServer } from './ide-server.js';
|
||||||
import semver from 'semver';
|
import semver from 'semver';
|
||||||
import { DiffContentProvider, DiffManager } from './diff-manager.js';
|
import { DiffContentProvider, DiffManager } from './diff-manager.js';
|
||||||
@@ -167,46 +166,6 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||||||
createWebViewProvider,
|
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(
|
context.subscriptions.push(
|
||||||
vscode.workspace.onDidCloseTextDocument((doc) => {
|
vscode.workspace.onDidCloseTextDocument((doc) => {
|
||||||
if (doc.uri.scheme === DIFF_SCHEME) {
|
if (doc.uri.scheme === DIFF_SCHEME) {
|
||||||
@@ -261,34 +220,42 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||||||
vscode.workspace.onDidGrantWorkspaceTrust(() => {
|
vscode.workspace.onDidGrantWorkspaceTrust(() => {
|
||||||
ideServer.syncEnvVars();
|
ideServer.syncEnvVars();
|
||||||
}),
|
}),
|
||||||
vscode.commands.registerCommand('qwen-code.runQwenCode', async () => {
|
vscode.commands.registerCommand(
|
||||||
const workspaceFolders = vscode.workspace.workspaceFolders;
|
'qwen-code.runQwenCode',
|
||||||
if (!workspaceFolders || workspaceFolders.length === 0) {
|
async (
|
||||||
vscode.window.showInformationMessage(
|
location?:
|
||||||
'No folder open. Please open a folder to run Qwen Code.',
|
| vscode.TerminalLocation
|
||||||
);
|
| vscode.TerminalEditorLocationOptions,
|
||||||
return;
|
) => {
|
||||||
}
|
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;
|
let selectedFolder: vscode.WorkspaceFolder | undefined;
|
||||||
if (workspaceFolders.length === 1) {
|
if (workspaceFolders.length === 1) {
|
||||||
selectedFolder = workspaceFolders[0];
|
selectedFolder = workspaceFolders[0];
|
||||||
} else {
|
} else {
|
||||||
selectedFolder = await vscode.window.showWorkspaceFolderPick({
|
selectedFolder = await vscode.window.showWorkspaceFolderPick({
|
||||||
placeHolder: 'Select a folder to run Qwen Code in',
|
placeHolder: 'Select a folder to run Qwen Code in',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedFolder) {
|
if (selectedFolder) {
|
||||||
const qwenCmd = 'qwen';
|
const qwenCmd = 'qwen';
|
||||||
const terminal = vscode.window.createTerminal({
|
const terminal = vscode.window.createTerminal({
|
||||||
name: `Qwen Code (${selectedFolder.name})`,
|
name: `Qwen Code (${selectedFolder.name})`,
|
||||||
cwd: selectedFolder.uri.fsPath,
|
cwd: selectedFolder.uri.fsPath,
|
||||||
});
|
location,
|
||||||
terminal.show();
|
});
|
||||||
terminal.sendText(qwenCmd);
|
terminal.show();
|
||||||
}
|
terminal.sendText(qwenCmd);
|
||||||
}),
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
vscode.commands.registerCommand('qwen-code.showNotices', async () => {
|
vscode.commands.registerCommand('qwen-code.showNotices', async () => {
|
||||||
const noticePath = vscode.Uri.joinPath(
|
const noticePath = vscode.Uri.joinPath(
|
||||||
context.extensionUri,
|
context.extensionUri,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
|
import { openChatCommand } from '../commands/index.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find the editor group immediately to the left of the Qwen chat webview.
|
* 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
|
// Make the chat group active by revealing the panel
|
||||||
try {
|
try {
|
||||||
await vscode.commands.executeCommand('qwenCode.openChat');
|
await vscode.commands.executeCommand(openChatCommand);
|
||||||
} catch {
|
} catch {
|
||||||
// Best-effort; continue even if this fails
|
// 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
|
// Restore focus to chat (optional), so we don't disturb user focus
|
||||||
try {
|
try {
|
||||||
await vscode.commands.executeCommand('qwenCode.openChat');
|
await vscode.commands.executeCommand(openChatCommand);
|
||||||
} catch {
|
} catch {
|
||||||
// Ignore
|
// 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,34 +43,78 @@ export class PanelManager {
|
|||||||
return false; // Panel already exists
|
return false; // Panel already exists
|
||||||
}
|
}
|
||||||
|
|
||||||
// We want the chat webview to live in a dedicated, locked group on the
|
// First, check if there's an existing Qwen Code group
|
||||||
// left. Create a new group on the far left and open the panel there.
|
const existingGroup = this.findExistingQwenCodeGroup();
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.panel = vscode.window.createWebviewPanel(
|
if (existingGroup) {
|
||||||
'qwenCode.chat',
|
// If Qwen Code webview already exists in a locked group, create the new panel in that same group
|
||||||
'Qwen Code',
|
console.log(
|
||||||
{ viewColumn: vscode.ViewColumn.One, preserveFocus: false }, // Focus and place in leftmost group
|
'[PanelManager] Found existing Qwen Code group, creating panel in same group',
|
||||||
{
|
);
|
||||||
enableScripts: true,
|
this.panel = vscode.window.createWebviewPanel(
|
||||||
retainContextWhenHidden: true,
|
'qwenCode.chat',
|
||||||
localResourceRoots: [
|
'Qwen Code',
|
||||||
vscode.Uri.joinPath(this.extensionUri, 'dist'),
|
{ viewColumn: existingGroup.viewColumn, preserveFocus: false },
|
||||||
vscode.Uri.joinPath(this.extensionUri, 'assets'),
|
{
|
||||||
],
|
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
|
// Set panel icon to Qwen logo
|
||||||
this.panel.iconPath = vscode.Uri.joinPath(
|
this.panel.iconPath = vscode.Uri.joinPath(
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { MessageHandler } from '../webview/MessageHandler.js';
|
|||||||
import { WebViewContent } from '../webview/WebViewContent.js';
|
import { WebViewContent } from '../webview/WebViewContent.js';
|
||||||
import { CliInstaller } from '../cli/cliInstaller.js';
|
import { CliInstaller } from '../cli/cliInstaller.js';
|
||||||
import { getFileName } from './utils/webviewUtils.js';
|
import { getFileName } from './utils/webviewUtils.js';
|
||||||
|
import { authMethod } from '../auth/index.js';
|
||||||
|
|
||||||
export class WebViewProvider {
|
export class WebViewProvider {
|
||||||
private panelManager: PanelManager;
|
private panelManager: PanelManager;
|
||||||
@@ -150,8 +151,85 @@ export class WebViewProvider {
|
|||||||
type: string;
|
type: string;
|
||||||
data: { optionId: string };
|
data: { optionId: string };
|
||||||
}) => {
|
}) => {
|
||||||
if (message.type === 'permissionResponse') {
|
if (message.type !== 'permissionResponse') return;
|
||||||
resolve(message.data.optionId);
|
|
||||||
|
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
|
// Store handler in message handler
|
||||||
@@ -339,10 +417,6 @@ export class WebViewProvider {
|
|||||||
|
|
||||||
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||||
const workingDir = workspaceFolder?.uri.fsPath || process.cwd();
|
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(
|
const hasValidAuth = await this.authStateManager.hasValidAuth(
|
||||||
workingDir,
|
workingDir,
|
||||||
authMethod,
|
authMethod,
|
||||||
@@ -392,83 +466,71 @@ export class WebViewProvider {
|
|||||||
!!this.authStateManager,
|
!!this.authStateManager,
|
||||||
);
|
);
|
||||||
|
|
||||||
const config = vscode.workspace.getConfiguration('qwenCode');
|
// Check if CLI is installed before attempting to connect
|
||||||
const qwenEnabled = config.get<boolean>('qwen.enabled', true);
|
const cliDetection = await CliDetector.detectQwenCli();
|
||||||
|
|
||||||
if (qwenEnabled) {
|
if (!cliDetection.isInstalled) {
|
||||||
// Check if CLI is installed before attempting to connect
|
console.log(
|
||||||
const cliDetection = await CliDetector.detectQwenCli();
|
'[WebViewProvider] Qwen CLI not detected, skipping agent connection',
|
||||||
|
);
|
||||||
|
console.log('[WebViewProvider] CLI detection error:', cliDetection.error);
|
||||||
|
|
||||||
if (!cliDetection.isInstalled) {
|
// Show VSCode notification with installation option
|
||||||
console.log(
|
await CliInstaller.promptInstallation();
|
||||||
'[WebViewProvider] Qwen CLI not detected, skipping agent connection',
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
'[WebViewProvider] CLI detection error:',
|
|
||||||
cliDetection.error,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Show VSCode notification with installation option
|
// Initialize empty conversation (can still browse history)
|
||||||
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
|
|
||||||
await this.initializeEmptyConversation();
|
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
|
// First, try to restore an existing session if we have cached auth
|
||||||
if (this.authStateManager) {
|
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(
|
const hasValidAuth = await this.authStateManager.hasValidAuth(
|
||||||
workingDir,
|
workingDir,
|
||||||
authMethod,
|
authMethod,
|
||||||
@@ -1001,6 +1059,29 @@ export class WebViewProvider {
|
|||||||
*/
|
*/
|
||||||
async createNewSession(): Promise<void> {
|
async createNewSession(): Promise<void> {
|
||||||
console.log('[WebViewProvider] Creating new session in current panel');
|
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 {
|
try {
|
||||||
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||||
const workingDir = workspaceFolder?.uri.fsPath || process.cwd();
|
const workingDir = workspaceFolder?.uri.fsPath || process.cwd();
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import React from 'react';
|
|||||||
import type { ToolCallData } from './toolcalls/shared/types.js';
|
import type { ToolCallData } from './toolcalls/shared/types.js';
|
||||||
import { FileLink } from './ui/FileLink.js';
|
import { FileLink } from './ui/FileLink.js';
|
||||||
import { useVSCode } from '../hooks/useVSCode.js';
|
import { useVSCode } from '../hooks/useVSCode.js';
|
||||||
|
import { handleOpenDiff } from '../utils/diffUtils.js';
|
||||||
|
|
||||||
interface InProgressToolCallProps {
|
interface InProgressToolCallProps {
|
||||||
toolCall: ToolCallData;
|
toolCall: ToolCallData;
|
||||||
@@ -138,19 +139,12 @@ export const InProgressToolCall: React.FC<InProgressToolCallProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle open diff
|
// Handle open diff
|
||||||
const handleOpenDiff = () => {
|
const handleOpenDiffInternal = () => {
|
||||||
if (!diffData) {
|
if (!diffData) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const path = diffData.path || filePath || '';
|
const path = diffData.path || filePath || '';
|
||||||
vscode.postMessage({
|
handleOpenDiff(vscode, path, diffData.oldText, diffData.newText);
|
||||||
type: 'openDiff',
|
|
||||||
data: {
|
|
||||||
path,
|
|
||||||
oldText: diffData.oldText || '',
|
|
||||||
newText: diffData.newText || '',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -179,7 +173,7 @@ export const InProgressToolCall: React.FC<InProgressToolCallProps> = ({
|
|||||||
{diffData && (
|
{diffData && (
|
||||||
<button
|
<button
|
||||||
type="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"
|
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
|
Open Diff
|
||||||
|
|||||||
@@ -165,8 +165,8 @@
|
|||||||
|
|
||||||
.markdown-content .code-block-wrapper pre {
|
.markdown-content .code-block-wrapper pre {
|
||||||
/* Reserve space so the copy button never overlaps code text */
|
/* Reserve space so the copy button never overlaps code text */
|
||||||
padding-top: 2rem; /* room for the button height */
|
padding-top: 1.5rem; /* Reduced padding - room for the button height */
|
||||||
padding-right: 2.4rem; /* room for the button width */
|
padding-right: 2rem; /* Reduced padding - room for the button width */
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-content .code-block-wrapper .copy-button {
|
.markdown-content .code-block-wrapper .copy-button {
|
||||||
@@ -178,21 +178,24 @@
|
|||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: 1px solid var(--app-primary-border-color);
|
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);
|
color: var(--app-secondary-foreground);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
opacity: 0; /* show on hover to reduce visual noise */
|
opacity: 0; /* show on hover to reduce visual noise */
|
||||||
transition: opacity 100ms ease-in-out;
|
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:hover .copy-button,
|
||||||
.markdown-content .code-block-wrapper .copy-button:focus {
|
.markdown-content .code-block-wrapper .copy-button:focus {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
pointer-events: auto; /* enable interaction when visible */
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-content .code-block-wrapper .copy-button:hover {
|
.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 {
|
.markdown-content .code-block-wrapper .copy-button:disabled {
|
||||||
|
|||||||
@@ -131,9 +131,15 @@ export const PermissionDrawer: React.FC<PermissionDrawerProps> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Escape to close
|
// Escape to cancel permission and close (align with CLI/Claude behavior)
|
||||||
if (e.key === 'Escape' && onClose) {
|
if (e.key === 'Escape') {
|
||||||
onClose();
|
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 (
|
return (
|
||||||
<button
|
<button
|
||||||
key={option.optionId}
|
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
|
isFocused
|
||||||
? 'text-[var(--app-list-active-foreground)] 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: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)}
|
onClick={() => onResponse(option.optionId)}
|
||||||
onMouseEnter={() => setFocusedIndex(index)}
|
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
|
* @license
|
||||||
* Copyright 2025 Qwen Team
|
* Copyright 2025 Qwen Team
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*
|
|
||||||
* Icons index - exports all icon components
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Types
|
|
||||||
export type { IconProps } from './types.js';
|
export type { IconProps } from './types.js';
|
||||||
|
|
||||||
// File icons
|
|
||||||
export { FileIcon, FileListIcon, FolderIcon } from './FileIcons.js';
|
export { FileIcon, FileListIcon, FolderIcon } from './FileIcons.js';
|
||||||
|
|
||||||
// Navigation icons
|
// Navigation icons
|
||||||
@@ -47,9 +42,6 @@ export {
|
|||||||
SelectionIcon,
|
SelectionIcon,
|
||||||
} from './StatusIcons.js';
|
} from './StatusIcons.js';
|
||||||
|
|
||||||
// Action icons
|
|
||||||
export { PlayIcon, SwitchIcon } from './ActionIcons.js';
|
|
||||||
|
|
||||||
// Special icons
|
// Special icons
|
||||||
export { ThinkingIcon, TerminalIcon } from './SpecialIcons.js';
|
export { ThinkingIcon, TerminalIcon } from './SpecialIcons.js';
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
} from '../shared/utils.js';
|
} from '../shared/utils.js';
|
||||||
import { useVSCode } from '../../../hooks/useVSCode.js';
|
import { useVSCode } from '../../../hooks/useVSCode.js';
|
||||||
import { FileLink } from '../../ui/FileLink.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)
|
* 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
|
// Group content by type; memoize to avoid new array identities on every render
|
||||||
const { errors, diffs } = useMemo(() => groupContent(content), [content]);
|
const { errors, diffs } = useMemo(() => groupContent(content), [content]);
|
||||||
// TODO:
|
const handleOpenDiffInternal = useCallback(
|
||||||
// console.log('EditToolCall', {
|
|
||||||
// content,
|
|
||||||
// locations,
|
|
||||||
// toolCallId,
|
|
||||||
// errors,
|
|
||||||
// diffs,
|
|
||||||
// });
|
|
||||||
const handleOpenDiff = useCallback(
|
|
||||||
(
|
(
|
||||||
path: string | undefined,
|
path: string | undefined,
|
||||||
oldText: string | null | undefined,
|
oldText: string | null | undefined,
|
||||||
newText: string | undefined,
|
newText: string | undefined,
|
||||||
) => {
|
) => {
|
||||||
if (path) {
|
handleOpenDiff(vscode, path, oldText, newText);
|
||||||
vscode.postMessage({
|
|
||||||
type: 'openDiff',
|
|
||||||
data: { path, oldText: oldText || '', newText: newText || '' },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[vscode],
|
[vscode],
|
||||||
);
|
);
|
||||||
@@ -74,24 +61,9 @@ export const EditToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
|||||||
// Extract filename from path
|
// Extract filename from path
|
||||||
const getFileName = (path: string): string => path.split('/').pop() || 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
|
// Automatically trigger openDiff when diff content is detected
|
||||||
// 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)
|
|
||||||
// Only trigger once per tool call by checking toolCallId
|
// Only trigger once per tool call by checking toolCallId
|
||||||
useEffect(() => {
|
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
|
// Only auto-open if there are diffs and we have the required data
|
||||||
if (diffs.length > 0) {
|
if (diffs.length > 0) {
|
||||||
const firstDiff = diffs[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
|
// Add a small delay to ensure the component is fully rendered
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
handleOpenDiff(path, firstDiff.oldText, firstDiff.newText);
|
handleOpenDiffInternal(path, firstDiff.oldText, firstDiff.newText);
|
||||||
// autoOpenedToolCallIds.add(toolCallId);
|
|
||||||
}, 100);
|
}, 100);
|
||||||
// Proper cleanup function
|
// Proper cleanup function
|
||||||
return () => timer && clearTimeout(timer);
|
return () => timer && clearTimeout(timer);
|
||||||
@@ -142,17 +113,11 @@ export const EditToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
|||||||
if (diffs.length > 0) {
|
if (diffs.length > 0) {
|
||||||
const firstDiff = diffs[0];
|
const firstDiff = diffs[0];
|
||||||
const path = firstDiff.path || (locations && locations[0]?.path) || '';
|
const path = firstDiff.path || (locations && locations[0]?.path) || '';
|
||||||
// const fileName = path ? getFileName(path) : '';
|
|
||||||
const summary = getDiffSummary(firstDiff.oldText, firstDiff.newText);
|
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);
|
const containerStatus = mapToolStatusToContainerStatus(toolCall.status);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`qwen-message message-item relative py-2 select-text cursor-pointer hover:bg-[var(--app-input-background)] toolcall-container toolcall-status-${containerStatus}`}
|
className={`qwen-message message-item relative py-2 select-text toolcall-container toolcall-status-${containerStatus}`}
|
||||||
// onClick={openFirstDiff}
|
|
||||||
title="Open diff in VS Code"
|
title="Open diff in VS Code"
|
||||||
>
|
>
|
||||||
{/* IMPORTANT: Always include min-w-0/max-w-full on inner wrappers to prevent overflow. */}
|
{/* 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 relative top-[-0.1em]">⎿</span>
|
||||||
<span className="flex-shrink-0 w-full">{summary}</span>
|
<span className="flex-shrink-0 w-full">{summary}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Show toolCallId only in development/debug mode */}
|
|
||||||
{toolCallId && isDevelopmentMode() && (
|
|
||||||
<span className="text-[10px] opacity-30">
|
|
||||||
[{toolCallId.slice(-8)}]
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
import { DiffDisplay } from './shared/DiffDisplay.js';
|
import { DiffDisplay } from './shared/DiffDisplay.js';
|
||||||
import { safeTitle, groupContent } from './shared/utils.js';
|
import { safeTitle, groupContent } from './shared/utils.js';
|
||||||
import { useVSCode } from '../../hooks/useVSCode.js';
|
import { useVSCode } from '../../hooks/useVSCode.js';
|
||||||
|
import { handleOpenDiff } from '../../utils/diffUtils.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generic tool call component that can display any tool call type
|
* 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
|
// Group content by type
|
||||||
const { textOutputs, errors, diffs } = groupContent(content);
|
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
|
// Error case: show operation + error in card layout
|
||||||
if (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
return (
|
return (
|
||||||
@@ -70,7 +58,7 @@ export const GenericToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
|||||||
oldText={item.oldText}
|
oldText={item.oldText}
|
||||||
newText={item.newText}
|
newText={item.newText}
|
||||||
onOpenDiff={() =>
|
onOpenDiff={() =>
|
||||||
handleOpenDiff(item.path, item.oldText, item.newText)
|
handleOpenDiff(vscode, item.path, item.oldText, item.newText)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
} from '../shared/utils.js';
|
} from '../shared/utils.js';
|
||||||
import { FileLink } from '../../ui/FileLink.js';
|
import { FileLink } from '../../ui/FileLink.js';
|
||||||
import { useVSCode } from '../../../hooks/useVSCode.js';
|
import { useVSCode } from '../../../hooks/useVSCode.js';
|
||||||
|
import { handleOpenDiff } from '../../../utils/diffUtils.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Specialized component for Read tool calls
|
* Specialized component for Read tool calls
|
||||||
@@ -30,18 +31,13 @@ export const ReadToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
|||||||
const { errors, diffs } = useMemo(() => groupContent(content), [content]);
|
const { errors, diffs } = useMemo(() => groupContent(content), [content]);
|
||||||
|
|
||||||
// Post a message to the extension host to open a VS Code diff tab
|
// Post a message to the extension host to open a VS Code diff tab
|
||||||
const handleOpenDiff = useCallback(
|
const handleOpenDiffInternal = useCallback(
|
||||||
(
|
(
|
||||||
path: string | undefined,
|
path: string | undefined,
|
||||||
oldText: string | null | undefined,
|
oldText: string | null | undefined,
|
||||||
newText: string | undefined,
|
newText: string | undefined,
|
||||||
) => {
|
) => {
|
||||||
if (path) {
|
handleOpenDiff(vscode, path, oldText, newText);
|
||||||
vscode.postMessage({
|
|
||||||
type: 'openDiff',
|
|
||||||
data: { path, oldText: oldText || '', newText: newText || '' },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[vscode],
|
[vscode],
|
||||||
);
|
);
|
||||||
@@ -59,7 +55,7 @@ export const ReadToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
|||||||
firstDiff.newText !== undefined
|
firstDiff.newText !== undefined
|
||||||
) {
|
) {
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
handleOpenDiff(path, firstDiff.oldText, firstDiff.newText);
|
handleOpenDiffInternal(path, firstDiff.oldText, firstDiff.newText);
|
||||||
}, 100);
|
}, 100);
|
||||||
return () => timer && clearTimeout(timer);
|
return () => timer && clearTimeout(timer);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
|
|
||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
import { FileLink } from '../../ui/FileLink.js';
|
import { FileLink } from '../../ui/FileLink.js';
|
||||||
import { isDevelopmentMode } from '../../../utils/envUtils.js';
|
|
||||||
import './LayoutComponents.css';
|
import './LayoutComponents.css';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -64,13 +63,6 @@ export const ToolCallContainer: React.FC<ToolCallContainerProps> = ({
|
|||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Show toolCallId only in development/debug mode */}
|
|
||||||
{_toolCallId && isDevelopmentMode() && (
|
|
||||||
<span className="text-[10px] opacity-30">
|
|
||||||
[{_toolCallId.slice(-8)}]
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import * as fs from 'fs';
|
|||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { BaseMessageHandler } from './BaseMessageHandler.js';
|
import { BaseMessageHandler } from './BaseMessageHandler.js';
|
||||||
import { FileOperations } from '../FileOperations.js';
|
|
||||||
import { getFileName } from '../utils/webviewUtils.js';
|
import { getFileName } from '../utils/webviewUtils.js';
|
||||||
|
import { showDiffCommand } from '../../commands/index.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* File message handler
|
* File message handler
|
||||||
@@ -316,14 +316,56 @@ export class FileMessageHandler extends BaseMessageHandler {
|
|||||||
/**
|
/**
|
||||||
* Open file
|
* Open file
|
||||||
*/
|
*/
|
||||||
private async handleOpenFile(path?: string): Promise<void> {
|
private async handleOpenFile(filePath?: string): Promise<void> {
|
||||||
if (!path) {
|
if (!filePath) {
|
||||||
console.warn('[FileMessageHandler] No path provided for openFile');
|
console.warn('[FileMessageHandler] No path provided for openFile');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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) {
|
} catch (error) {
|
||||||
console.error('[FileMessageHandler] Failed to open file:', error);
|
console.error('[FileMessageHandler] Failed to open file:', error);
|
||||||
vscode.window.showErrorMessage(`Failed to open file: ${error}`);
|
vscode.window.showErrorMessage(`Failed to open file: ${error}`);
|
||||||
@@ -342,7 +384,7 @@ export class FileMessageHandler extends BaseMessageHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await vscode.commands.executeCommand('qwenCode.showDiff', {
|
await vscode.commands.executeCommand(showDiffCommand, {
|
||||||
path: (data.path as string) || '',
|
path: (data.path as string) || '',
|
||||||
oldText: (data.oldText as string) || '',
|
oldText: (data.oldText as string) || '',
|
||||||
newText: (data.newText 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 {
|
export default {
|
||||||
content: [
|
content: [
|
||||||
// Progressive adoption strategy: Only scan newly created Tailwind components
|
// Progressive adoption strategy: Only scan newly created Tailwind components
|
||||||
'./src/webview/App.tsx',
|
// './src/webview/App.tsx',
|
||||||
'./src/webview/components/ui/**/*.{js,jsx,ts,tsx}',
|
'./src/webview/**/*.{js,jsx,ts,tsx}',
|
||||||
'./src/webview/components/messages/**/*.{js,jsx,ts,tsx}',
|
// './src/webview/components/messages/**/*.{js,jsx,ts,tsx}',
|
||||||
'./src/webview/components/toolcalls/**/*.{js,jsx,ts,tsx}',
|
// './src/webview/components/toolcalls/**/*.{js,jsx,ts,tsx}',
|
||||||
'./src/webview/components/InProgressToolCall.tsx',
|
// './src/webview/components/InProgressToolCall.tsx',
|
||||||
'./src/webview/components/MessageContent.tsx',
|
// './src/webview/components/MessageContent.tsx',
|
||||||
'./src/webview/components/InputForm.tsx',
|
// './src/webview/components/InputForm.tsx',
|
||||||
'./src/webview/components/PermissionDrawer.tsx',
|
// './src/webview/components/PermissionDrawer.tsx',
|
||||||
'./src/webview/components/PlanDisplay.tsx',
|
// './src/webview/components/PlanDisplay.tsx',
|
||||||
'./src/webview/components/session/SessionSelector.tsx',
|
// './src/webview/components/session/SessionSelector.tsx',
|
||||||
'./src/webview/components/messages/UserMessage.tsx',
|
// './src/webview/components/messages/UserMessage.tsx',
|
||||||
],
|
],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
|
|||||||
Reference in New Issue
Block a user