Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
f1cca06
feat(vscode-ide-companion): wip configurable layout settings
yiliang114 Jan 14, 2026
be2d097
Merge branch 'main' into feat/vscode-ide-companion-layout
yiliang114 Mar 6, 2026
c10aa7e
feat(vscode-ide-companion/layout): add sidebar view and simplify chat…
yiliang114 Mar 6, 2026
2220936
fix(vscode-ide-companion): fix PanelManager race condition and add se…
yiliang114 Mar 7, 2026
9b05ae1
fix(vscode-ide-companion): fix PanelManager captureTab race condition
yiliang114 Mar 7, 2026
dc1fe6b
Merge branch 'main' of https://github.com/QwenLM/qwen-code into feat/…
yiliang114 Mar 7, 2026
1354308
chore(vscode-ide-companion): update lock file after merge
yiliang114 Mar 7, 2026
2334386
Merge branch 'main' into feat/vscode-ide-companion-layout
yiliang114 Mar 9, 2026
b26e21f
feat(vscode-ide-companion): cherry-pick ACP compatibility improvement…
yiliang114 Mar 9, 2026
3bb21c3
refactor(vscode-ide-companion): unify view ID naming and add multi-vi…
yiliang114 Mar 9, 2026
3f77f41
Merge branch 'main' of https://github.com/QwenLM/qwen-code into feat/…
yiliang114 Mar 10, 2026
64b2dfa
Merge remote-tracking branch 'origin/main' into feat/vscode-ide-compa…
yiliang114 Mar 10, 2026
e9f57e6
Merge branch 'main' of https://github.com/QwenLM/qwen-code into feat/…
yiliang114 Mar 11, 2026
40afc13
feat(vscode-ide-companion): refactor webview layout to mutual-exclusi…
yiliang114 Mar 11, 2026
e1e0eb8
fix(vscode-ide-companion): improve thinking message ordering and cont…
yiliang114 Mar 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/vscode-ide-companion/assets/sidebar-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
63 changes: 62 additions & 1 deletion packages/vscode-ide-companion/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,13 @@
"ide companion"
],
"activationEvents": [
"onStartupFinished"
"onStartupFinished",
"onView:qwen-code.chatView.sidebar",
"onView:qwen-code.chatView.secondary",
"onCommand:qwen-code.openChat",
"onCommand:qwen-code.focusChat",
"onCommand:qwen-code.newConversation",
"onCommand:qwen-code.showLogs"
],
"contributes": {
"jsonValidation": [
Expand All @@ -37,6 +43,44 @@
"url": "./schemas/settings.schema.json"
}
],
"viewsContainers": {
"activitybar": [
{
"id": "qwen-code-sidebar",
"title": "Qwen Code",
"icon": "assets/sidebar-icon.svg",
"when": "qwen-code:doesNotSupportSecondarySidebar"
}
],
"secondarySidebar": [
{
"id": "qwen-code-secondary",
"title": "Qwen Code",
"icon": "assets/sidebar-icon.svg",
"when": "!qwen-code:doesNotSupportSecondarySidebar"
}
]
},
"views": {
"qwen-code-sidebar": [
{
"type": "webview",
"id": "qwen-code.chatView.sidebar",
"name": "Qwen Code",
"icon": "assets/sidebar-icon.svg",
"when": "qwen-code:doesNotSupportSecondarySidebar"
}
],
"qwen-code-secondary": [
{
"type": "webview",
"id": "qwen-code.chatView.secondary",
"name": "Qwen Code",
"icon": "assets/sidebar-icon.svg",
"when": "!qwen-code:doesNotSupportSecondarySidebar"
}
]
},
"languages": [
{
"id": "qwen-diff-editable"
Expand Down Expand Up @@ -69,6 +113,18 @@
{
"command": "qwen-code.login",
"title": "Qwen Code: Login"
},
{
"command": "qwen-code.focusChat",
"title": "Qwen Code: Focus Chat View"
},
{
"command": "qwen-code.newConversation",
"title": "Qwen Code: New Conversation"
},
{
"command": "qwen-code.showLogs",
"title": "Qwen Code: Show Logs"
}
],
"menus": {
Expand Down Expand Up @@ -113,6 +169,11 @@
"command": "qwen.diff.accept",
"key": "cmd+s",
"when": "qwen.diff.isVisible"
},
{
"command": "qwen-code.focusChat",
"key": "ctrl+shift+l",
"mac": "cmd+shift+l"
}
]
},
Expand Down
124 changes: 124 additions & 0 deletions packages/vscode-ide-companion/src/commands/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/

import { beforeEach, describe, expect, it, vi } from 'vitest';
import {
focusChatCommand,
openNewChatTabCommand,
registerNewCommands,
} from './index.js';

const {
registerCommand,
executeCommand,
showWarningMessage,
showInformationMessage,
} = vi.hoisted(() => ({
registerCommand: vi.fn(
(_id: string, handler: (...args: unknown[]) => unknown) => ({
dispose: vi.fn(),
handler,
}),
),
executeCommand: vi.fn(),
showWarningMessage: vi.fn(),
showInformationMessage: vi.fn(),
}));

vi.mock('vscode', () => ({
commands: {
registerCommand,
executeCommand,
},
window: {
showWarningMessage,
showInformationMessage,
},
workspace: {
workspaceFolders: [],
},
Uri: {
joinPath: vi.fn(),
},
}));

function getRegisteredHandler(commandId: string) {
const call = registerCommand.mock.calls.find(([id]) => id === commandId);
if (!call) {
throw new Error(`Command ${commandId} was not registered`);
}
return call[1] as (...args: unknown[]) => Promise<void>;
}

describe('registerNewCommands', () => {
const context = { subscriptions: [] as Array<{ dispose: () => void }> };
const diffManager = { showDiff: vi.fn() };
const log = vi.fn();

beforeEach(() => {
context.subscriptions = [];
registerCommand.mockClear();
executeCommand.mockClear();
showWarningMessage.mockClear();
showInformationMessage.mockClear();
});

it('openNewChatTab opens a new provider without creating a second session explicitly', async () => {
const provider = {
show: vi.fn().mockResolvedValue(undefined),
createNewSession: vi.fn().mockResolvedValue(undefined),
};

registerNewCommands(
context as never,
log,
diffManager as never,
() => [],
() => provider as never,
);

await getRegisteredHandler(openNewChatTabCommand)();

expect(provider.show).toHaveBeenCalledTimes(1);
expect(provider.createNewSession).not.toHaveBeenCalled();
});

it('focusChat focuses the secondary sidebar when it is supported', async () => {
registerNewCommands(
context as never,
log,
diffManager as never,
() => [],
vi.fn() as never,
undefined,
true,
);

await getRegisteredHandler(focusChatCommand)();

expect(executeCommand).toHaveBeenCalledWith(
'qwen-code.chatView.secondary.focus',
);
});

it('focusChat falls back to the primary sidebar when secondary sidebar is unavailable', async () => {
registerNewCommands(
context as never,
log,
diffManager as never,
() => [],
vi.fn() as never,
undefined,
false,
);

await getRegisteredHandler(focusChatCommand)();

expect(executeCommand).toHaveBeenCalledWith(
'qwen-code.chatView.sidebar.focus',
);
});
});
62 changes: 60 additions & 2 deletions packages/vscode-ide-companion/src/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@

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,
CHAT_VIEW_ID_SECONDARY,
} from '../constants/viewIds.js';

type Logger = (message: string) => void;

Expand All @@ -15,16 +19,36 @@ export const showDiffCommand = 'qwenCode.showDiff';
export const openChatCommand = 'qwen-code.openChat';
export const openNewChatTabCommand = 'qwenCode.openNewChatTab';
export const loginCommand = 'qwen-code.login';
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.
*
* `openChat` and `newConversation` always open an editor tab, while
* `focusChat` focuses the secondary sidebar (preferred) or primary sidebar.
*
* @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
* @param supportsSecondarySidebar - Whether the running VS Code supports secondary sidebar
*/
export function registerNewCommands(
context: vscode.ExtensionContext,
log: Logger,
diffManager: DiffManager,
getWebViewProviders: () => WebViewProvider[],
createWebViewProvider: () => WebViewProvider,
outputChannel?: vscode.OutputChannel,
supportsSecondarySidebar = true,
): 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 providers = getWebViewProviders();
Expand Down Expand Up @@ -62,10 +86,10 @@ export function registerNewCommands(
),
);

// Open New Chat Tab: always create a new editor tab
disposables.push(
vscode.commands.registerCommand(openNewChatTabCommand, async () => {
const provider = createWebViewProvider();
// Session restoration is now disabled by default, so no need to suppress it
await provider.show();
}),
);
Expand All @@ -82,5 +106,39 @@ export function registerNewCommands(
}
}),
);

// Focus Chat: bring the active chat view to front.
// Use secondary sidebar when supported; fall back to primary sidebar.
disposables.push(
vscode.commands.registerCommand(focusChatCommand, async () => {
if (supportsSecondarySidebar) {
await vscode.commands.executeCommand(`${CHAT_VIEW_ID_SECONDARY}.focus`);
} else {
await vscode.commands.executeCommand(`${CHAT_VIEW_ID_SIDEBAR}.focus`);
}
}),
);

// 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);
}
17 changes: 17 additions & 0 deletions packages/vscode-ide-companion/src/constants/viewIds.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/

/**
* WebviewView IDs for the chat UI host positions.
* These IDs must match the `views` contributions declared in package.json.
*
* Only one of sidebar / secondary is visible at runtime — controlled by the
* `qwen-code:doesNotSupportSecondarySidebar` context key in package.json.
* The secondary sidebar is preferred; the primary sidebar is a fallback for
* VS Code versions that lack secondary sidebar support.
*/
export const CHAT_VIEW_ID_SIDEBAR = 'qwen-code.chatView.sidebar';
export const CHAT_VIEW_ID_SECONDARY = 'qwen-code.chatView.secondary';
20 changes: 20 additions & 0 deletions packages/vscode-ide-companion/src/extension.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ vi.mock('@qwen-code/qwen-code-core/src/ide/detect-ide.js', async () => {
});

vi.mock('vscode', () => ({
version: '1.94.0',
window: {
createOutputChannel: vi.fn(() => ({
appendLine: vi.fn(),
Expand All @@ -43,6 +44,9 @@ vi.mock('vscode', () => ({
registerWebviewPanelSerializer: vi.fn(() => ({
dispose: vi.fn(),
})),
registerWebviewViewProvider: vi.fn(() => ({
dispose: vi.fn(),
})),
},
workspace: {
workspaceFolders: [],
Expand Down Expand Up @@ -134,6 +138,22 @@ describe('activate', () => {
expect(vscode.workspace.onDidGrantWorkspaceTrust).toHaveBeenCalled();
});

it('should register webview view providers for sidebar and secondary positions', async () => {
await activate(context);

// Verify registerWebviewViewProvider was called 2 times (sidebar + secondary)
const registerCalls = vi.mocked(vscode.window.registerWebviewViewProvider)
.mock.calls;
expect(registerCalls).toHaveLength(2);

// Extract view IDs from the calls
const viewIds = registerCalls.map((call) => call[0]);

// Only sidebar and secondary are registered; panel view was removed
expect(viewIds).toContain('qwen-code.chatView.sidebar');
expect(viewIds).toContain('qwen-code.chatView.secondary');
});

it('should launch the Qwen Code when the user clicks the button', async () => {
const showInformationMessageMock = vi
.mocked(vscode.window.showInformationMessage)
Expand Down
Loading
Loading