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;