From c10aa7ebe5a99cf66eb6826b2cc7579825654278 Mon Sep 17 00:00:00 2001
From: yiliang114 <1204183885@qq.com>
Date: Sat, 7 Mar 2026 00:30:40 +0800
Subject: [PATCH] feat(vscode-ide-companion/layout): add sidebar view and
simplify chat positioning
All chat positions (sidebar, editor tab, panel, secondary sidebar) are now
available simultaneously. Remove the old chat.location configuration and
setChatLocation commands. Add focusChat, newConversation, and showLogs commands.
Refactor ChatWebviewViewProvider to use lazy factory pattern and move webview
files into providers/ subdirectory.
---
.../assets/sidebar-icon.svg | 6 +
packages/vscode-ide-companion/package.json | 92 ++++------
.../src/commands/index.ts | 137 +++++---------
.../src/constants/viewIds.ts | 13 ++
.../src/extension.test.ts | 3 +
.../vscode-ide-companion/src/extension.ts | 169 +++---------------
.../src/utils/editorGroupUtils.ts | 16 +-
.../src/webview/ChatWebviewViewProvider.ts | 41 -----
.../providers/ChatWebviewViewProvider.ts | 47 +++++
.../webview/{ => providers}/MessageHandler.ts | 8 +-
.../webview/{ => providers}/PanelManager.ts | 0
.../webview/{ => providers}/WebViewContent.ts | 2 +-
.../{ => providers}/WebViewProvider.ts | 22 +--
13 files changed, 190 insertions(+), 366 deletions(-)
create mode 100644 packages/vscode-ide-companion/assets/sidebar-icon.svg
create mode 100644 packages/vscode-ide-companion/src/constants/viewIds.ts
delete mode 100644 packages/vscode-ide-companion/src/webview/ChatWebviewViewProvider.ts
create mode 100644 packages/vscode-ide-companion/src/webview/providers/ChatWebviewViewProvider.ts
rename packages/vscode-ide-companion/src/webview/{ => providers}/MessageHandler.ts (84%)
rename packages/vscode-ide-companion/src/webview/{ => providers}/PanelManager.ts (100%)
rename packages/vscode-ide-companion/src/webview/{ => providers}/WebViewContent.ts (96%)
rename packages/vscode-ide-companion/src/webview/{ => providers}/WebViewProvider.ts (98%)
diff --git a/packages/vscode-ide-companion/assets/sidebar-icon.svg b/packages/vscode-ide-companion/assets/sidebar-icon.svg
new file mode 100644
index 000000000..51cdab785
--- /dev/null
+++ b/packages/vscode-ide-companion/assets/sidebar-icon.svg
@@ -0,0 +1,6 @@
+
diff --git a/packages/vscode-ide-companion/package.json b/packages/vscode-ide-companion/package.json
index 37152f8b4..358aa018a 100644
--- a/packages/vscode-ide-companion/package.json
+++ b/packages/vscode-ide-companion/package.json
@@ -31,60 +31,60 @@
"onStartupFinished",
"onView:qwenCode.chatView.panel",
"onView:qwenCode.chatView.secondary",
- "onCommand:qwen-code.setChatLocation.editor",
- "onCommand:qwen-code.setChatLocation.panel",
- "onCommand:qwen-code.setChatLocation.secondary",
- "onCommand:qwen-code.openChat"
+ "onView:qwenCode.chatView.sidebar",
+ "onCommand:qwen-code.openChat",
+ "onCommand:qwen-code.focusChat",
+ "onCommand:qwen-code.newConversation",
+ "onCommand:qwen-code.showLogs"
],
"contributes": {
"configuration": {
"title": "Qwen Code Companion",
- "properties": {
- "qwen-code.chat.location": {
- "type": "string",
- "default": "editor",
- "enum": [
- "editor",
- "panel",
- "secondary"
- ],
- "description": "选择聊天界面放置的位置:编辑器标签页(默认)、底部面板或 Secondary Side Bar。修改后需重新打开聊天。"
- }
- }
+ "properties": {}
},
"viewsContainers": {
+ "activitybar": [
+ {
+ "id": "qwenCodeSidebar",
+ "title": "Qwen Code",
+ "icon": "assets/sidebar-icon.svg"
+ }
+ ],
"panel": [
{
"id": "qwenCodePanel",
"title": "Qwen Code",
- "icon": "assets/icon.png",
- "when": "qwenCode.chatLocation == 'panel'"
+ "icon": "assets/icon.png"
}
],
"secondarySidebar": [
{
"id": "qwenCodeSecondary",
"title": "Qwen Code",
- "icon": "assets/icon.png",
- "when": "qwenCode.chatLocation == 'secondary'"
+ "icon": "assets/icon.png"
}
]
},
"views": {
+ "qwenCodeSidebar": [
+ {
+ "type": "webview",
+ "id": "qwenCode.chatView.sidebar",
+ "name": "Qwen Code"
+ }
+ ],
"qwenCodePanel": [
{
"id": "qwenCode.chatView.panel",
"name": "Qwen Code",
- "icon": "assets/icon.png",
- "when": "qwenCode.chatLocation == 'panel'"
+ "icon": "assets/icon.png"
}
],
"qwenCodeSecondary": [
{
"id": "qwenCode.chatView.secondary",
"name": "Qwen Code",
- "icon": "assets/icon.png",
- "when": "qwenCode.chatLocation == 'secondary'"
+ "icon": "assets/icon.png"
}
]
},
@@ -122,22 +122,16 @@
"title": "Qwen Code: Login"
},
{
- "command": "qwen-code.setChatLocation.editor",
- "title": "Qwen Code: 将聊天放在编辑器标签页"
+ "command": "qwen-code.focusChat",
+ "title": "Qwen Code: Focus Chat Input"
},
{
- "command": "qwen-code.setChatLocation.panel",
- "title": "Qwen Code: 将聊天放在底部面板"
+ "command": "qwen-code.newConversation",
+ "title": "Qwen Code: New Conversation"
},
{
- "command": "qwen-code.setChatLocation.secondary",
- "title": "Qwen Code: 将聊天放在 Secondary Bar"
- }
- ],
- "submenus": [
- {
- "id": "qwenCode.chatLocationMenu",
- "label": "Qwen Code: 聊天位置"
+ "command": "qwen-code.showLogs",
+ "title": "Qwen Code: Show Logs"
}
],
"menus": {
@@ -169,27 +163,6 @@
{
"command": "qwen-code.openChat",
"group": "navigation"
- },
- {
- "submenu": "qwenCode.chatLocationMenu",
- "group": "navigation@2"
- }
- ],
- "qwenCode.chatLocationMenu": [
- {
- "command": "qwen-code.setChatLocation.editor",
- "group": "1_editor",
- "when": "qwenCode.chatLocation != 'editor'"
- },
- {
- "command": "qwen-code.setChatLocation.panel",
- "group": "2_panel",
- "when": "qwenCode.chatLocation != 'panel'"
- },
- {
- "command": "qwen-code.setChatLocation.secondary",
- "group": "3_secondary",
- "when": "qwenCode.chatLocation != 'secondary'"
}
]
},
@@ -203,6 +176,11 @@
"command": "qwen.diff.accept",
"key": "cmd+s",
"when": "qwen.diff.isVisible"
+ },
+ {
+ "command": "qwen-code.focusChat",
+ "key": "ctrl+shift+l",
+ "mac": "cmd+shift+l"
}
]
},
diff --git a/packages/vscode-ide-companion/src/commands/index.ts b/packages/vscode-ide-companion/src/commands/index.ts
index 0a04fafe1..3af162bd3 100644
--- a/packages/vscode-ide-companion/src/commands/index.ts
+++ b/packages/vscode-ide-companion/src/commands/index.ts
@@ -6,40 +6,47 @@
import * as vscode from 'vscode';
import type { DiffManager } from '../diff-manager.js';
-import type { WebViewProvider } from '../webview/WebViewProvider.js';
+import type { WebViewProvider } from '../webview/providers/WebViewProvider.js';
+import { CHAT_VIEW_ID_SIDEBAR } from '../constants/viewIds.js';
type Logger = (message: string) => void;
-export type ChatHostLocation = 'editor' | 'panel' | 'secondary';
export const runQwenCodeCommand = 'qwen-code.runQwenCode';
export const showDiffCommand = 'qwenCode.showDiff';
export const openChatCommand = 'qwen-code.openChat';
export const openNewChatTabCommand = 'qwenCode.openNewChatTab';
export const loginCommand = 'qwen-code.login';
-export const setChatLocationEditorCommand = 'qwen-code.setChatLocation.editor';
-export const setChatLocationPanelCommand = 'qwen-code.setChatLocation.panel';
-export const setChatLocationSecondaryCommand =
- 'qwen-code.setChatLocation.secondary';
+export const focusChatCommand = 'qwen-code.focusChat';
+export const newConversationCommand = 'qwen-code.newConversation';
+export const showLogsCommand = 'qwen-code.showLogs';
+/**
+ * Register all Qwen Code chat-related commands.
+ *
+ * All chat positions (editor tab, sidebar, panel, secondary sidebar) are
+ * available simultaneously. `openChat` and `newConversation` always open an
+ * editor tab, while `focusChat` focuses the primary sidebar view.
+ *
+ * @param context - VS Code extension context for subscription management
+ * @param log - Logger function for debug output
+ * @param diffManager - Diff manager for showing file diffs
+ * @param getWebViewProviders - Returns all active editor-tab WebView providers
+ * @param createWebViewProvider - Factory to create a new editor-tab WebView provider
+ * @param outputChannel - Optional output channel for the showLogs command
+ */
export function registerNewCommands(
context: vscode.ExtensionContext,
log: Logger,
diffManager: DiffManager,
getWebViewProviders: () => WebViewProvider[],
createWebViewProvider: () => WebViewProvider,
- getChatHostLocation: () => ChatHostLocation,
- focusChatView: () => Promise,
+ outputChannel?: vscode.OutputChannel,
): void {
const disposables: vscode.Disposable[] = [];
+ // Open Chat: show the most recent editor tab or create a new one
disposables.push(
vscode.commands.registerCommand(openChatCommand, async () => {
- const host = getChatHostLocation();
- if (host !== 'editor') {
- await focusChatView();
- return;
- }
-
const providers = getWebViewProviders();
if (providers.length > 0) {
await providers[providers.length - 1].show();
@@ -75,19 +82,10 @@ export function registerNewCommands(
),
);
+ // Open New Chat Tab: always create a new editor tab
disposables.push(
vscode.commands.registerCommand(openNewChatTabCommand, async () => {
- const host = getChatHostLocation();
- if (host !== 'editor') {
- vscode.window.showInformationMessage(
- '当前配置使用面板/Secondary Bar 承载聊天,暂不支持多标签。将为你聚焦现有聊天视图。',
- );
- await focusChatView();
- return;
- }
-
const provider = createWebViewProvider();
- // Session restoration is now disabled by default, so no need to suppress it
await provider.show();
}),
);
@@ -105,74 +103,33 @@ export function registerNewCommands(
}),
);
- const updateChatLocation = async (location: ChatHostLocation) => {
- const current = getChatHostLocation();
- if (current === location) {
- log(`[Command] Chat location unchanged (${location}), focusing view`);
- await focusChatView();
- return;
- }
-
- const target: vscode.ConfigurationTarget =
- vscode.workspace.workspaceFolders?.length && vscode.workspace.name
- ? vscode.ConfigurationTarget.Workspace
- : vscode.ConfigurationTarget.Global;
-
- try {
- await vscode.workspace
- .getConfiguration('qwen-code')
- .update('chat.location', location, target);
- log(
- `[Command] Chat location set to ${location} (target=${target}; prev=${current})`,
- );
- // Update context immediately so view container visibility switches without reload
- await vscode.commands.executeCommand(
- 'setContext',
- 'qwenCode.chatLocation',
- location,
- );
-
- // Dispose existing editor-hosted chat panels when moving to panel/secondary to avoid confusion.
- if (location !== 'editor') {
- for (const provider of getWebViewProviders()) {
- try {
- provider.getPanel()?.dispose();
- } catch (error) {
- log(
- `[Command] Failed to dispose chat panel during relocation: ${error}`,
- );
- }
- }
- }
-
- // Small delay to ensure context has updated before focusing
- await new Promise((resolve) => setTimeout(resolve, 100));
- await focusChatView();
- void vscode.window.showInformationMessage(
- `聊天位置已切换为 ${location === 'editor' ? '编辑器标签页' : location === 'panel' ? '底部面板' : 'Secondary Bar'},已尝试为你打开对应位置。如未生效,请重载窗口。`,
- );
- } catch (error) {
- const msg = error instanceof Error ? error.message : String(error);
- log(`[Command] Failed to set chat location: ${msg}`);
- void vscode.window.showErrorMessage(
- `切换聊天位置失败:${msg}。可在 Settings 搜索 "Qwen Code Chat Location" 手动修改。`,
- );
- }
- };
-
+ // Focus Chat: bring the primary sidebar chat view to front
disposables.push(
- vscode.commands.registerCommand(setChatLocationEditorCommand, async () => {
- await updateChatLocation('editor');
+ vscode.commands.registerCommand(focusChatCommand, async () => {
+ await vscode.commands.executeCommand(`${CHAT_VIEW_ID_SIDEBAR}.focus`);
}),
- vscode.commands.registerCommand(setChatLocationPanelCommand, async () => {
- await updateChatLocation('panel');
- }),
- vscode.commands.registerCommand(
- setChatLocationSecondaryCommand,
- async () => {
- await updateChatLocation('secondary');
- },
- ),
);
+
+ // New Conversation: open a new editor tab for a fresh conversation
+ disposables.push(
+ vscode.commands.registerCommand(newConversationCommand, async () => {
+ const provider = createWebViewProvider();
+ await provider.show();
+ }),
+ );
+
+ // Show Logs: reveal the output channel
+ disposables.push(
+ vscode.commands.registerCommand(showLogsCommand, async () => {
+ if (outputChannel) {
+ outputChannel.show(true);
+ } else {
+ vscode.window.showWarningMessage(
+ 'Qwen Code Companion log channel is not available.',
+ );
+ }
+ }),
+ );
+
context.subscriptions.push(...disposables);
}
diff --git a/packages/vscode-ide-companion/src/constants/viewIds.ts b/packages/vscode-ide-companion/src/constants/viewIds.ts
new file mode 100644
index 000000000..96862d058
--- /dev/null
+++ b/packages/vscode-ide-companion/src/constants/viewIds.ts
@@ -0,0 +1,13 @@
+/**
+ * @license
+ * Copyright 2025 Qwen Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/**
+ * WebviewView IDs for the three host positions where the chat UI can appear.
+ * These IDs must match the `views` contributions declared in package.json.
+ */
+export const CHAT_VIEW_ID_PANEL = 'qwenCode.chatView.panel';
+export const CHAT_VIEW_ID_SECONDARY = 'qwenCode.chatView.secondary';
+export const CHAT_VIEW_ID_SIDEBAR = 'qwenCode.chatView.sidebar';
diff --git a/packages/vscode-ide-companion/src/extension.test.ts b/packages/vscode-ide-companion/src/extension.test.ts
index ef0d5ad46..d6128f91e 100644
--- a/packages/vscode-ide-companion/src/extension.test.ts
+++ b/packages/vscode-ide-companion/src/extension.test.ts
@@ -43,6 +43,9 @@ vi.mock('vscode', () => ({
registerWebviewPanelSerializer: vi.fn(() => ({
dispose: vi.fn(),
})),
+ registerWebviewViewProvider: vi.fn(() => ({
+ dispose: vi.fn(),
+ })),
},
workspace: {
workspaceFolders: [],
diff --git a/packages/vscode-ide-companion/src/extension.ts b/packages/vscode-ide-companion/src/extension.ts
index 0efe48c6b..55c3f5299 100644
--- a/packages/vscode-ide-companion/src/extension.ts
+++ b/packages/vscode-ide-companion/src/extension.ts
@@ -14,43 +14,20 @@ import {
IDE_DEFINITIONS,
type IdeInfo,
} from '@qwen-code/qwen-code-core/src/ide/detect-ide.js';
-import { WebViewProvider } from './webview/WebViewProvider.js';
+import { WebViewProvider } from './webview/providers/WebViewProvider.js';
+import { ChatWebviewViewProvider } from './webview/providers/ChatWebviewViewProvider.js';
import {
- ChatWebviewViewProvider,
CHAT_VIEW_ID_PANEL,
CHAT_VIEW_ID_SECONDARY,
-} from './webview/ChatWebviewViewProvider.js';
-import {
- registerNewCommands,
- type ChatHostLocation,
-} from './commands/index.js';
+ CHAT_VIEW_ID_SIDEBAR,
+} from './constants/viewIds.js';
+import { registerNewCommands } from './commands/index.js';
import { ReadonlyFileSystemProvider } from './services/readonlyFileSystemProvider.js';
import { isWindows } from './utils/platform.js';
const CLI_IDE_COMPANION_IDENTIFIER = 'qwenlm.qwen-code-vscode-ide-companion';
const INFO_MESSAGE_SHOWN_KEY = 'qwenCodeInfoMessageShown';
export const DIFF_SCHEME = 'qwen-diff';
-const CHAT_LOCATION_SETTING_KEY = 'chat.location';
-
-function getChatHostLocation(): ChatHostLocation {
- const location = vscode.workspace
- .getConfiguration('qwen-code')
- .get(CHAT_LOCATION_SETTING_KEY, 'editor');
-
- if (location === 'panel' || location === 'secondary') {
- return location;
- }
- return 'editor';
-}
-
-async function setChatLocationContext(location: ChatHostLocation) {
- console.log('[Extension] setChatLocationContext ->', location);
- await vscode.commands.executeCommand(
- 'setContext',
- 'qwenCode.chatLocation',
- location,
- );
-}
/**
* IDE environments where the installation greeting is hidden. In these
@@ -172,9 +149,6 @@ export async function activate(context: vscode.ExtensionContext) {
},
);
- const initialChatLocation = getChatHostLocation();
- await setChatLocationContext(initialChatLocation);
-
// Helper function to create a new WebView provider instance
const createWebViewProvider = (): WebViewProvider => {
const provider = new WebViewProvider(context, context.extensionUri);
@@ -182,43 +156,24 @@ export async function activate(context: vscode.ExtensionContext) {
return provider;
};
- // Register WebviewView hosts (panel and secondary sidebar). They are shown based on config conditions.
- const chatViewProviderPanel = new ChatWebviewViewProvider(
- createWebViewProvider(),
- context.extensionUri,
- 'panel',
- );
- const chatViewProviderSecondary = new ChatWebviewViewProvider(
- createWebViewProvider(),
- context.extensionUri,
- 'secondary',
- );
- console.log(
- '[Extension] Registering WebviewView providers for panel and secondary',
- );
- const panelProviderDisposable = vscode.window.registerWebviewViewProvider(
+ // Register WebviewView hosts for all positions (sidebar, panel, secondary).
+ // Providers are lazily instantiated — the factory is only called when VS Code
+ // actually opens the view, keeping startup lightweight.
+ const chatViewIds = [
+ CHAT_VIEW_ID_SIDEBAR,
CHAT_VIEW_ID_PANEL,
- chatViewProviderPanel,
- {
- webviewOptions: { retainContextWhenHidden: true },
- },
- );
- const secondaryProviderDisposable = vscode.window.registerWebviewViewProvider(
CHAT_VIEW_ID_SECONDARY,
- chatViewProviderSecondary,
- {
- webviewOptions: { retainContextWhenHidden: true },
- },
- );
- console.log(
- '[Extension] WebviewView providers registered:',
- panelProviderDisposable ? 'panel' : 'panel (missing)',
- secondaryProviderDisposable ? 'secondary' : 'secondary (missing)',
- );
- context.subscriptions.push(
- panelProviderDisposable,
- secondaryProviderDisposable,
- );
+ ] as const;
+
+ for (const viewId of chatViewIds) {
+ context.subscriptions.push(
+ vscode.window.registerWebviewViewProvider(
+ viewId,
+ new ChatWebviewViewProvider(createWebViewProvider),
+ { webviewOptions: { retainContextWhenHidden: true } },
+ ),
+ );
+ }
// Register WebView panel serializer for persistence across reloads
context.subscriptions.push(
@@ -257,43 +212,6 @@ export async function activate(context: vscode.ExtensionContext) {
}),
);
- const focusChatView = async () => {
- const host = getChatHostLocation();
- if (host === 'editor') {
- const providers = webViewProviders;
- if (providers.length > 0) {
- await providers[providers.length - 1].show();
- } else {
- const provider = createWebViewProvider();
- await provider.show();
- }
- return;
- }
-
- const viewId =
- host === 'secondary' ? CHAT_VIEW_ID_SECONDARY : CHAT_VIEW_ID_PANEL;
- try {
- // Ensure the view provider is registered and ready before focusing
- await new Promise((resolve) => setTimeout(resolve, 200));
- await vscode.commands.executeCommand(`${viewId}.focus`);
-
- // Wait a bit more to ensure the view has initialized
- await new Promise((resolve) => setTimeout(resolve, 300));
- } catch (err) {
- log(`Failed to focus chat view (${viewId}): ${err}`);
- // Try to ensure the view provider exists by creating new ones if needed
- try {
- const currentProviders = webViewProviders;
- if (currentProviders.length === 0) {
- createWebViewProvider(); // Create a new provider if none exist
- }
- await vscode.commands.executeCommand(`${viewId}.focus`);
- } catch (retryErr) {
- log(`Retry failed to focus chat view (${viewId}): ${retryErr}`);
- }
- }
- };
-
// Register newly added commands via commands module
registerNewCommands(
context,
@@ -301,8 +219,7 @@ export async function activate(context: vscode.ExtensionContext) {
diffManager,
() => webViewProviders,
createWebViewProvider,
- getChatHostLocation,
- focusChatView,
+ logger,
);
context.subscriptions.push(
@@ -311,48 +228,6 @@ export async function activate(context: vscode.ExtensionContext) {
diffManager.cancelDiff(doc.uri);
}
}),
- vscode.workspace.onDidChangeConfiguration(async (event) => {
- if (event.affectsConfiguration('qwen-code.chat.location')) {
- const newLocation = getChatHostLocation();
- log(
- `[Extension] Configuration changed: chat.location -> ${newLocation}`,
- );
- await setChatLocationContext(newLocation);
- if (newLocation !== 'editor') {
- for (const provider of webViewProviders) {
- try {
- provider.getPanel()?.dispose();
- } catch (err) {
- log(
- `[Extension] Failed to dispose chat panel after location change: ${err}`,
- );
- }
- }
- }
-
- // Focus the appropriate view after config change
- if (newLocation === 'panel' || newLocation === 'secondary') {
- const viewId =
- newLocation === 'secondary'
- ? CHAT_VIEW_ID_SECONDARY
- : CHAT_VIEW_ID_PANEL;
- try {
- // Give VS Code a moment to update the context and UI
- await new Promise((resolve) => setTimeout(resolve, 300));
- await vscode.commands.executeCommand(`${viewId}.focus`);
-
- // Refresh the view to ensure content appears
- await new Promise((resolve) => setTimeout(resolve, 200));
- // Trigger a re-render by focusing again after a delay
- await vscode.commands.executeCommand(`${viewId}.focus`);
- } catch (err) {
- log(
- `[Extension] Failed to focus chat view after location change: ${err}`,
- );
- }
- }
- }
- }),
vscode.workspace.registerTextDocumentContentProvider(
DIFF_SCHEME,
diffContentProvider,
diff --git a/packages/vscode-ide-companion/src/utils/editorGroupUtils.ts b/packages/vscode-ide-companion/src/utils/editorGroupUtils.ts
index 13897412c..e855b2dec 100644
--- a/packages/vscode-ide-companion/src/utils/editorGroupUtils.ts
+++ b/packages/vscode-ide-companion/src/utils/editorGroupUtils.ts
@@ -7,23 +7,13 @@
import * as vscode from 'vscode';
import { openChatCommand } from '../commands/index.js';
-function isEditorHostedChat(): boolean {
- const location = vscode.workspace
- .getConfiguration('qwen-code')
- .get('chat.location', 'editor');
- return location === 'editor';
-}
-
/**
* Find the editor group immediately to the left of the Qwen chat webview.
* - If the chat webview group is the leftmost group, returns undefined.
+ * - If no chat webview is found in any editor group, returns undefined.
* - Uses the webview tab viewType 'mainThreadWebview-qwenCode.chat'.
*/
export function findLeftGroupOfChatWebview(): vscode.ViewColumn | undefined {
- if (!isEditorHostedChat()) {
- return undefined;
- }
-
try {
const groups = vscode.window.tabGroups.all;
@@ -103,10 +93,6 @@ function waitForTabGroupsCondition(
export async function ensureLeftGroupOfChatWebview(): Promise<
vscode.ViewColumn | undefined
> {
- if (!isEditorHostedChat()) {
- return undefined;
- }
-
// First try to find an existing left neighbor
const existing = findLeftGroupOfChatWebview();
if (existing !== undefined) {
diff --git a/packages/vscode-ide-companion/src/webview/ChatWebviewViewProvider.ts b/packages/vscode-ide-companion/src/webview/ChatWebviewViewProvider.ts
deleted file mode 100644
index 5eb906ac5..000000000
--- a/packages/vscode-ide-companion/src/webview/ChatWebviewViewProvider.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-/**
- * @license
- * Copyright 2025 Qwen Team
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import * as vscode from 'vscode';
-import type { WebViewProvider } from './WebViewProvider.js';
-
-export const CHAT_VIEW_ID_PANEL = 'qwenCode.chatView.panel';
-export const CHAT_VIEW_ID_SECONDARY = 'qwenCode.chatView.secondary';
-
-/**
- * WebviewView host for placing the chat UI in panel/secondary sidebar.
- */
-export class ChatWebviewViewProvider implements vscode.WebviewViewProvider {
- constructor(
- private readonly webViewProvider: WebViewProvider,
- private readonly extensionUri: vscode.Uri,
- private readonly hostKind: 'panel' | 'secondary',
- ) {}
-
- async resolveWebviewView(webviewView: vscode.WebviewView): Promise {
- console.log(
- `[ChatWebviewViewProvider] resolveWebviewView invoked for ${webviewView.viewType} (host=${this.hostKind})`,
- );
- // Determine the view ID from the webviewView
- const viewId = webviewView.viewType; // This will be either 'qwenCode.chatView.panel' or 'qwenCode.chatView.secondary'
-
- // Ensure scripts/resources are allowed
- webviewView.webview.options = {
- enableScripts: true,
- localResourceRoots: [
- vscode.Uri.joinPath(this.extensionUri, 'dist'),
- vscode.Uri.joinPath(this.extensionUri, 'assets'),
- ],
- };
-
- await this.webViewProvider.attachToView(webviewView, viewId);
- }
-}
diff --git a/packages/vscode-ide-companion/src/webview/providers/ChatWebviewViewProvider.ts b/packages/vscode-ide-companion/src/webview/providers/ChatWebviewViewProvider.ts
new file mode 100644
index 000000000..ffce1152a
--- /dev/null
+++ b/packages/vscode-ide-companion/src/webview/providers/ChatWebviewViewProvider.ts
@@ -0,0 +1,47 @@
+/**
+ * @license
+ * Copyright 2025 Qwen Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type * as vscode from 'vscode';
+import type { WebViewProvider } from './WebViewProvider.js';
+
+/**
+ * Factory function type that lazily creates a WebViewProvider instance.
+ * The provider is only instantiated when VS Code actually opens the view.
+ */
+export type WebViewProviderFactory = () => WebViewProvider;
+
+/**
+ * WebviewView host for placing the chat UI in sidebar / panel / secondary sidebar.
+ *
+ * Accepts a factory function instead of a pre-built WebViewProvider so the
+ * heavyweight provider (QwenAgentManager, ConversationStore, etc.) is only
+ * created when VS Code actually opens the view, not at extension startup.
+ */
+export class ChatWebviewViewProvider implements vscode.WebviewViewProvider {
+ private webViewProvider: WebViewProvider | null = null;
+
+ /**
+ * @param createWebViewProvider - Factory that creates a WebViewProvider on demand
+ */
+ constructor(private readonly createWebViewProvider: WebViewProviderFactory) {}
+
+ /**
+ * Called by VS Code when the webview view becomes visible for the first time.
+ * Creates the WebViewProvider lazily and attaches the webview.
+ *
+ * @param webviewView - The webview view created by VS Code
+ */
+ async resolveWebviewView(webviewView: vscode.WebviewView): Promise {
+ // Lazily create the provider on first resolve
+ if (!this.webViewProvider) {
+ this.webViewProvider = this.createWebViewProvider();
+ }
+
+ // Webview options (enableScripts, localResourceRoots) are configured
+ // inside WebViewProvider.attachToView — no duplication needed here.
+ await this.webViewProvider.attachToView(webviewView, webviewView.viewType);
+ }
+}
diff --git a/packages/vscode-ide-companion/src/webview/MessageHandler.ts b/packages/vscode-ide-companion/src/webview/providers/MessageHandler.ts
similarity index 84%
rename from packages/vscode-ide-companion/src/webview/MessageHandler.ts
rename to packages/vscode-ide-companion/src/webview/providers/MessageHandler.ts
index 30b9abe56..7507ad4e1 100644
--- a/packages/vscode-ide-companion/src/webview/MessageHandler.ts
+++ b/packages/vscode-ide-companion/src/webview/providers/MessageHandler.ts
@@ -4,10 +4,10 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import type { QwenAgentManager } from '../services/qwenAgentManager.js';
-import type { ConversationStore } from '../services/conversationStore.js';
-import type { PermissionResponseMessage } from '../types/webviewMessageTypes.js';
-import { MessageRouter } from './handlers/MessageRouter.js';
+import type { QwenAgentManager } from '../../services/qwenAgentManager.js';
+import type { ConversationStore } from '../../services/conversationStore.js';
+import type { PermissionResponseMessage } from '../../types/webviewMessageTypes.js';
+import { MessageRouter } from '../handlers/MessageRouter.js';
/**
* MessageHandler (Refactored Version)
diff --git a/packages/vscode-ide-companion/src/webview/PanelManager.ts b/packages/vscode-ide-companion/src/webview/providers/PanelManager.ts
similarity index 100%
rename from packages/vscode-ide-companion/src/webview/PanelManager.ts
rename to packages/vscode-ide-companion/src/webview/providers/PanelManager.ts
diff --git a/packages/vscode-ide-companion/src/webview/WebViewContent.ts b/packages/vscode-ide-companion/src/webview/providers/WebViewContent.ts
similarity index 96%
rename from packages/vscode-ide-companion/src/webview/WebViewContent.ts
rename to packages/vscode-ide-companion/src/webview/providers/WebViewContent.ts
index f3f934890..bdc71e0ba 100644
--- a/packages/vscode-ide-companion/src/webview/WebViewContent.ts
+++ b/packages/vscode-ide-companion/src/webview/providers/WebViewContent.ts
@@ -5,7 +5,7 @@
*/
import * as vscode from 'vscode';
-import { escapeHtml } from './utils/webviewUtils.js';
+import { escapeHtml } from '../utils/webviewUtils.js';
/**
* WebView HTML Content Generator
diff --git a/packages/vscode-ide-companion/src/webview/WebViewProvider.ts b/packages/vscode-ide-companion/src/webview/providers/WebViewProvider.ts
similarity index 98%
rename from packages/vscode-ide-companion/src/webview/WebViewProvider.ts
rename to packages/vscode-ide-companion/src/webview/providers/WebViewProvider.ts
index 5a9bd4346..d926b8315 100644
--- a/packages/vscode-ide-companion/src/webview/WebViewProvider.ts
+++ b/packages/vscode-ide-companion/src/webview/providers/WebViewProvider.ts
@@ -5,17 +5,17 @@
*/
import * as vscode from 'vscode';
-import { QwenAgentManager } from '../services/qwenAgentManager.js';
-import { ConversationStore } from '../services/conversationStore.js';
-import type { AcpPermissionRequest } from '../types/acpTypes.js';
-import type { ModelInfo } from '../types/acpTypes.js';
-import type { PermissionResponseMessage } from '../types/webviewMessageTypes.js';
-import { PanelManager } from '../webview/PanelManager.js';
-import { MessageHandler } from '../webview/MessageHandler.js';
-import { WebViewContent } from '../webview/WebViewContent.js';
-import { getFileName } from './utils/webviewUtils.js';
-import { type ApprovalModeValue } from '../types/approvalModeValueTypes.js';
-import { isAuthenticationRequiredError } from '../utils/authErrors.js';
+import { QwenAgentManager } from '../../services/qwenAgentManager.js';
+import { ConversationStore } from '../../services/conversationStore.js';
+import type { AcpPermissionRequest } from '../../types/acpTypes.js';
+import type { ModelInfo } from '../../types/acpTypes.js';
+import type { PermissionResponseMessage } from '../../types/webviewMessageTypes.js';
+import { PanelManager } from './PanelManager.js';
+import { MessageHandler } from './MessageHandler.js';
+import { WebViewContent } from './WebViewContent.js';
+import { getFileName } from '../utils/webviewUtils.js';
+import { type ApprovalModeValue } from '../../types/approvalModeValueTypes.js';
+import { isAuthenticationRequiredError } from '../../utils/authErrors.js';
export class WebViewProvider {
private panelManager: PanelManager;