From 3605f817796c45e81819244ec642a3236e850963 Mon Sep 17 00:00:00 2001 From: Dragon <52599892+DragonnZhang@users.noreply.github.com> Date: Tue, 19 May 2026 06:56:16 +0800 Subject: [PATCH] chore(vscode): run dev cli from source (#4283) --- packages/vscode-ide-companion/development.md | 6 +++ .../webview/providers/WebViewProvider.test.ts | 42 ++++++++++++++++++- .../src/webview/providers/WebViewProvider.ts | 32 +++++++++++--- scripts/dev.js | 12 +++++- 4 files changed, 84 insertions(+), 8 deletions(-) diff --git a/packages/vscode-ide-companion/development.md b/packages/vscode-ide-companion/development.md index 4a3f33e32f..8f67c6eb16 100644 --- a/packages/vscode-ide-companion/development.md +++ b/packages/vscode-ide-companion/development.md @@ -15,6 +15,12 @@ To run the extension locally for development, we recommend using the automatic w ``` 4. **Launch Host:** Press **`F5`** (or **`fn+F5`** on Mac) to open a new **Extension Development Host** window with the extension running. +When the extension runs in VS Code's development mode, the chat panel starts +the CLI through the repository root `scripts/dev.js` entrypoint. CLI and core +source changes are picked up on the next ACP process start without rebuilding +or copying the bundled `dist/qwen-cli/cli.js`. Extension UI changes still need +the watch process above. + ### Manual Build If you only need to compile the extension once without watching for changes: diff --git a/packages/vscode-ide-companion/src/webview/providers/WebViewProvider.test.ts b/packages/vscode-ide-companion/src/webview/providers/WebViewProvider.test.ts index f87bd35cf4..67b1080f20 100644 --- a/packages/vscode-ide-companion/src/webview/providers/WebViewProvider.test.ts +++ b/packages/vscode-ide-companion/src/webview/providers/WebViewProvider.test.ts @@ -126,6 +126,11 @@ vi.mock('@qwen-code/qwen-code-core', async () => { }); vi.mock('vscode', () => ({ + ExtensionMode: { + Production: 1, + Development: 2, + Test: 3, + }, ConfigurationTarget: { Global: 'global', }, @@ -330,11 +335,14 @@ vi.mock('../../utils/errorMessage.js', () => ({ getErrorMessage: vi.fn((error: unknown) => String(error)), })); -import { WebViewProvider } from './WebViewProvider.js'; +import { WebViewProvider, resolveQwenCliEntryPath } from './WebViewProvider.js'; import { truncatePanelTitle, MAX_PANEL_TITLE_LENGTH, } from '../utils/panelTitleUtils.js'; +import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import * as path from 'node:path'; const createConfigChangeEvent = (...affectedSections: string[]) => ({ affectsConfiguration: (section: string) => affectedSections.includes(section), @@ -345,6 +353,38 @@ type WebViewMessageHandler = (message: { data?: unknown; }) => Promise; +describe('resolveQwenCliEntryPath', () => { + it('uses the source dev entry when the extension runs in development mode', () => { + const tempRoot = mkdtempSync(path.join(tmpdir(), 'qwen-vscode-dev-')); + try { + const extensionRoot = path.join( + tempRoot, + 'packages', + 'vscode-ide-companion', + ); + const devEntry = path.join(tempRoot, 'scripts', 'dev.js'); + mkdirSync(extensionRoot, { recursive: true }); + mkdirSync(path.dirname(devEntry), { recursive: true }); + writeFileSync(devEntry, ''); + + expect( + resolveQwenCliEntryPath({ fsPath: extensionRoot } as never, 2), + ).toBe(devEntry); + } finally { + rmSync(tempRoot, { recursive: true, force: true }); + } + }); + + it('uses the bundled CLI outside development mode', () => { + expect( + resolveQwenCliEntryPath( + { fsPath: '/extension-root' } as never, + undefined, + ), + ).toBe('/extension-root/dist/qwen-cli/cli.js'); + }); +}); + /** * Create a mock webview + provider and attach them. * If `captureMessageHandler` is true, the `onDidReceiveMessage` handler is diff --git a/packages/vscode-ide-companion/src/webview/providers/WebViewProvider.ts b/packages/vscode-ide-companion/src/webview/providers/WebViewProvider.ts index d6ada5b8b1..fef26a2055 100644 --- a/packages/vscode-ide-companion/src/webview/providers/WebViewProvider.ts +++ b/packages/vscode-ide-companion/src/webview/providers/WebViewProvider.ts @@ -6,6 +6,8 @@ import * as vscode from 'vscode'; import { execFile } from 'child_process'; +import { existsSync } from 'node:fs'; +import * as path from 'node:path'; import { QwenAgentManager } from '../../services/qwenAgentManager.js'; import { ConversationStore } from '../../services/conversationStore.js'; import type { @@ -60,6 +62,26 @@ const AUTH_RELATED_QWEN_SETTINGS = [ 'qwen-code.codingPlanRegion', ] as const; +export function resolveQwenCliEntryPath( + extensionUri: vscode.Uri, + extensionMode: vscode.ExtensionMode | undefined, +): string { + if (extensionMode === vscode.ExtensionMode.Development) { + const devCliEntry = path.resolve( + extensionUri.fsPath, + '..', + '..', + 'scripts', + 'dev.js', + ); + if (existsSync(devCliEntry)) { + return devCliEntry; + } + } + + return vscode.Uri.joinPath(extensionUri, 'dist', 'qwen-cli', 'cli.js').fsPath; +} + function isInsightCommand(command: string): boolean { const [firstToken = ''] = command.trim().split(/\s+/, 1); return firstToken.replace(/^\/+/, '') === 'insight'; @@ -1206,12 +1228,10 @@ export class WebViewProvider { `[WebViewProvider] Using CLI-managed authentication (autoAuth=${autoAuthenticate})`, ); - const bundledCliEntry = vscode.Uri.joinPath( + const cliEntry = resolveQwenCliEntryPath( this.extensionUri, - 'dist', - 'qwen-cli', - 'cli.js', - ).fsPath; + this.context.extensionMode, + ); try { console.log('[WebViewProvider] Connecting to agent...'); @@ -1219,7 +1239,7 @@ export class WebViewProvider { // Pass the detected CLI path to ensure we use the correct installation const connectResult = await this.agentManager.connect( workingDir, - bundledCliEntry, + cliEntry, options, ); console.log('[WebViewProvider] Agent connected successfully'); diff --git a/scripts/dev.js b/scripts/dev.js index bcbe27d894..9e4b92d580 100644 --- a/scripts/dev.js +++ b/scripts/dev.js @@ -109,7 +109,17 @@ const env = { // On Windows, use tsx.cmd; on Unix, use tsx directly const isWin = platform() === 'win32'; -const tsxCmd = isWin ? 'tsx.cmd' : 'tsx'; +const localTsxCmd = join( + root, + 'node_modules', + '.bin', + isWin ? 'tsx.cmd' : 'tsx', +); +const tsxCmd = existsSync(localTsxCmd) + ? localTsxCmd + : isWin + ? 'tsx.cmd' + : 'tsx'; const tsxArgs = [cliEntry, ...process.argv.slice(2)]; const child = spawn(tsxCmd, tsxArgs, {