From b4747c23c8094d948e6cd45196150cb0fcaee583 Mon Sep 17 00:00:00 2001 From: yiliang114 <1204183885@qq.com> Date: Thu, 14 May 2026 10:15:17 +0800 Subject: [PATCH 1/6] fix(vscode-ide-companion): use existing editor group for diff instead of forcing a new one MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the chat webview is in the leftmost group, opening a diff previously called ensureLeftGroupOfChatWebview() which forcibly created a new editor group. This was disruptive UX — there is often an existing empty group to the right that could be reused. Change the fallback chain from "left neighbor → force-create → Beside" to "left neighbor → right neighbor → Beside". Also apply the same fix to the readonly file opener in FileMessageHandler. --- .../vscode-ide-companion/src/diff-manager.ts | 19 ++-- .../src/utils/editorGroupUtils.ts | 95 +++++++++++-------- .../webview/handlers/FileMessageHandler.ts | 10 +- 3 files changed, 66 insertions(+), 58 deletions(-) diff --git a/packages/vscode-ide-companion/src/diff-manager.ts b/packages/vscode-ide-companion/src/diff-manager.ts index 8367517abd6..9574c1bff58 100644 --- a/packages/vscode-ide-companion/src/diff-manager.ts +++ b/packages/vscode-ide-companion/src/diff-manager.ts @@ -14,7 +14,7 @@ import * as vscode from 'vscode'; import { DIFF_SCHEME } from './extension.js'; import { findLeftGroupOfChatWebview, - ensureLeftGroupOfChatWebview, + findRightGroupOfChatWebview, } from './utils/editorGroupUtils.js'; export class DiffContentProvider implements vscode.TextDocumentContentProvider { @@ -222,17 +222,12 @@ export class DiffManager { true, ); - // Prefer opening the diff adjacent to the chat webview (so we don't - // replace content inside the locked webview group). We try the group to - // the left of the chat webview first; if none exists we fall back to - // ViewColumn.Beside. With the chat locked in the leftmost group, this - // fallback opens diffs to the right of the chat. - let targetViewColumn = findLeftGroupOfChatWebview(); - if (targetViewColumn === undefined) { - // If there is no left neighbor, create one to satisfy the requirement of - // opening diffs to the left of the chat webview. - targetViewColumn = await ensureLeftGroupOfChatWebview(); - } + // Prefer opening the diff in the group to the left of the chat webview. + // When that isn't available (e.g. chat is in the leftmost group), try the + // group to the right so we reuse existing layout. Only fall back to + // ViewColumn.Beside when neither neighbour exists. + const targetViewColumn = + findLeftGroupOfChatWebview() ?? findRightGroupOfChatWebview(); await vscode.commands.executeCommand( 'vscode.diff', diff --git a/packages/vscode-ide-companion/src/utils/editorGroupUtils.ts b/packages/vscode-ide-companion/src/utils/editorGroupUtils.ts index e855b2dec04..3401842101f 100644 --- a/packages/vscode-ide-companion/src/utils/editorGroupUtils.ts +++ b/packages/vscode-ide-companion/src/utils/editorGroupUtils.ts @@ -7,49 +7,75 @@ import * as vscode from 'vscode'; import { openChatCommand } from '../commands/index.js'; +const CHAT_WEBVIEW_TYPE = 'mainThreadWebview-qwenCode.chat'; + +function isChatWebview(tab: vscode.Tab): boolean { + const input: unknown = (tab as { input?: unknown }).input; + return ( + !!input && + typeof input === 'object' && + (input as { viewType: string }).viewType === CHAT_WEBVIEW_TYPE + ); +} + +function findWebviewGroup(): vscode.TabGroup | undefined { + return vscode.window.tabGroups.all.find((group) => + group.tabs.some(isChatWebview), + ); +} + /** * 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 { try { - const groups = vscode.window.tabGroups.all; - - // Locate the group that contains our chat webview - const webviewGroup = groups.find((group) => - group.tabs.some((tab) => { - const input: unknown = (tab as { input?: unknown }).input; - const isWebviewInput = (inp: unknown): inp is { viewType: string } => - !!inp && typeof inp === 'object' && 'viewType' in inp; - return ( - isWebviewInput(input) && - input.viewType === 'mainThreadWebview-qwenCode.chat' - ); - }), - ); - + const webviewGroup = findWebviewGroup(); if (!webviewGroup) { return undefined; } - // Among all groups to the left (smaller viewColumn), choose the one with - // the largest viewColumn value (i.e. the immediate neighbor on the left). - let candidate: - | { group: vscode.TabGroup; viewColumn: vscode.ViewColumn } - | undefined; - for (const g of groups) { - if (g.viewColumn < webviewGroup.viewColumn) { - if (!candidate || g.viewColumn > candidate.viewColumn) { - candidate = { group: g, viewColumn: g.viewColumn }; - } + // Among groups with smaller viewColumn, pick the largest (closest neighbor). + let candidate: vscode.ViewColumn | undefined; + for (const g of vscode.window.tabGroups.all) { + if ( + g.viewColumn < webviewGroup.viewColumn && + (candidate === undefined || g.viewColumn > candidate) + ) { + candidate = g.viewColumn; } } + return candidate; + } catch (_err) { + return undefined; + } +} + +/** + * Find the editor group immediately to the right of the Qwen chat webview. + * - If the chat webview group is the rightmost group, returns undefined. + * - If no chat webview is found in any editor group, returns undefined. + */ +export function findRightGroupOfChatWebview(): vscode.ViewColumn | undefined { + try { + const webviewGroup = findWebviewGroup(); + if (!webviewGroup) { + return undefined; + } - return candidate?.viewColumn; + // Among groups with larger viewColumn, pick the smallest (closest neighbor). + let candidate: vscode.ViewColumn | undefined; + for (const g of vscode.window.tabGroups.all) { + if ( + g.viewColumn > webviewGroup.viewColumn && + (candidate === undefined || g.viewColumn < candidate) + ) { + candidate = g.viewColumn; + } + } + return candidate; } catch (_err) { - // Best-effort only; fall back to default behavior if anything goes wrong return undefined; } } @@ -100,18 +126,7 @@ export async function ensureLeftGroupOfChatWebview(): Promise< } // Locate the chat webview group - const groups = vscode.window.tabGroups.all; - const webviewGroup = groups.find((group) => - group.tabs.some((tab) => { - const input: unknown = (tab as { input?: unknown }).input; - const isWebviewInput = (inp: unknown): inp is { viewType: string } => - !!inp && typeof inp === 'object' && 'viewType' in inp; - return ( - isWebviewInput(input) && - input.viewType === 'mainThreadWebview-qwenCode.chat' - ); - }), - ); + const webviewGroup = findWebviewGroup(); if (!webviewGroup) { return undefined; diff --git a/packages/vscode-ide-companion/src/webview/handlers/FileMessageHandler.ts b/packages/vscode-ide-companion/src/webview/handlers/FileMessageHandler.ts index f8708d8d4d3..54f5e238f92 100644 --- a/packages/vscode-ide-companion/src/webview/handlers/FileMessageHandler.ts +++ b/packages/vscode-ide-companion/src/webview/handlers/FileMessageHandler.ts @@ -10,7 +10,7 @@ import { getFileName } from '../utils/webviewUtils.js'; import { showDiffCommand } from '../../commands/index.js'; import { findLeftGroupOfChatWebview, - ensureLeftGroupOfChatWebview, + findRightGroupOfChatWebview, } from '../../utils/editorGroupUtils.js'; import { ReadonlyFileSystemProvider } from '../../services/readonlyFileSystemProvider.js'; import { FileDiscoveryService } from '@qwen-code/qwen-code-core/src/services/fileDiscoveryService.js'; @@ -673,11 +673,9 @@ export class FileMessageHandler extends BaseMessageHandler { return; } - // Find or ensure left group of chat webview - let targetViewColumn = findLeftGroupOfChatWebview(); - if (targetViewColumn === undefined) { - targetViewColumn = await ensureLeftGroupOfChatWebview(); - } + // Find the nearest editor group to the left or right of the chat webview + const targetViewColumn = + findLeftGroupOfChatWebview() ?? findRightGroupOfChatWebview(); // Open as readonly document in the left group and focus it (single click should be enough) const document = await vscode.workspace.openTextDocument(uri); From 756e9cf2e8d6e8b7e8dbbadfd12da3079e138912 Mon Sep 17 00:00:00 2001 From: yiliang114 <1204183885@qq.com> Date: Thu, 14 May 2026 11:45:29 +0800 Subject: [PATCH 2/6] =?UTF-8?q?fix(vscode-ide-companion):=20address=20revi?= =?UTF-8?q?ew=20feedback=20=E2=80=94=20explicit=20Beside=20fallback,=20sha?= =?UTF-8?q?red=20scan=20helper,=20comment=20accuracy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add ?? vscode.ViewColumn.Beside to targetViewColumn declaration so the fallback is explicit even if the downstream usage is reached without it - Extract findNeighborGroup helper to de-duplicate the near-identical scan loops in findLeftGroupOfChatWebview and findRightGroupOfChatWebview - Update stale comment in FileMessageHandler to reflect that the readonly document may open in the right group, not only the left --- .../vscode-ide-companion/src/diff-manager.ts | 4 +- .../src/utils/editorGroupUtils.ts | 42 ++++++++++--------- .../webview/handlers/FileMessageHandler.ts | 2 +- 3 files changed, 26 insertions(+), 22 deletions(-) diff --git a/packages/vscode-ide-companion/src/diff-manager.ts b/packages/vscode-ide-companion/src/diff-manager.ts index 9574c1bff58..af4edcc9472 100644 --- a/packages/vscode-ide-companion/src/diff-manager.ts +++ b/packages/vscode-ide-companion/src/diff-manager.ts @@ -227,7 +227,9 @@ export class DiffManager { // group to the right so we reuse existing layout. Only fall back to // ViewColumn.Beside when neither neighbour exists. const targetViewColumn = - findLeftGroupOfChatWebview() ?? findRightGroupOfChatWebview(); + findLeftGroupOfChatWebview() ?? + findRightGroupOfChatWebview() ?? + vscode.ViewColumn.Beside; await vscode.commands.executeCommand( 'vscode.diff', diff --git a/packages/vscode-ide-companion/src/utils/editorGroupUtils.ts b/packages/vscode-ide-companion/src/utils/editorGroupUtils.ts index 3401842101f..32badcce9f1 100644 --- a/packages/vscode-ide-companion/src/utils/editorGroupUtils.ts +++ b/packages/vscode-ide-companion/src/utils/editorGroupUtils.ts @@ -24,6 +24,20 @@ function findWebviewGroup(): vscode.TabGroup | undefined { ); } +function findNeighborGroup( + isOnSide: (v: vscode.ViewColumn) => boolean, + isCloser: (cur: vscode.ViewColumn, cand: vscode.ViewColumn) => boolean, +): vscode.ViewColumn | undefined { + let candidate: vscode.ViewColumn | undefined; + for (const g of vscode.window.tabGroups.all) { + if (!isOnSide(g.viewColumn)) continue; + if (candidate === undefined || isCloser(candidate, g.viewColumn)) { + candidate = g.viewColumn; + } + } + return candidate; +} + /** * Find the editor group immediately to the left of the Qwen chat webview. * - If the chat webview group is the leftmost group, returns undefined. @@ -37,16 +51,10 @@ export function findLeftGroupOfChatWebview(): vscode.ViewColumn | undefined { } // Among groups with smaller viewColumn, pick the largest (closest neighbor). - let candidate: vscode.ViewColumn | undefined; - for (const g of vscode.window.tabGroups.all) { - if ( - g.viewColumn < webviewGroup.viewColumn && - (candidate === undefined || g.viewColumn > candidate) - ) { - candidate = g.viewColumn; - } - } - return candidate; + return findNeighborGroup( + (v) => v < webviewGroup.viewColumn, + (_cur, cand) => cand > _cur, + ); } catch (_err) { return undefined; } @@ -65,16 +73,10 @@ export function findRightGroupOfChatWebview(): vscode.ViewColumn | undefined { } // Among groups with larger viewColumn, pick the smallest (closest neighbor). - let candidate: vscode.ViewColumn | undefined; - for (const g of vscode.window.tabGroups.all) { - if ( - g.viewColumn > webviewGroup.viewColumn && - (candidate === undefined || g.viewColumn < candidate) - ) { - candidate = g.viewColumn; - } - } - return candidate; + return findNeighborGroup( + (v) => v > webviewGroup.viewColumn, + (_cur, cand) => cand < _cur, + ); } catch (_err) { return undefined; } diff --git a/packages/vscode-ide-companion/src/webview/handlers/FileMessageHandler.ts b/packages/vscode-ide-companion/src/webview/handlers/FileMessageHandler.ts index 54f5e238f92..1a8714484e2 100644 --- a/packages/vscode-ide-companion/src/webview/handlers/FileMessageHandler.ts +++ b/packages/vscode-ide-companion/src/webview/handlers/FileMessageHandler.ts @@ -677,7 +677,7 @@ export class FileMessageHandler extends BaseMessageHandler { const targetViewColumn = findLeftGroupOfChatWebview() ?? findRightGroupOfChatWebview(); - // Open as readonly document in the left group and focus it (single click should be enough) + // Open as readonly document in the selected neighboring group and focus it (single click should be enough) const document = await vscode.workspace.openTextDocument(uri); await vscode.window.showTextDocument(document, { viewColumn: targetViewColumn ?? vscode.ViewColumn.Beside, From a7abe8573fbf7f36a3dcf984f41a5d39bf216d6a Mon Sep 17 00:00:00 2001 From: yiliang114 <1204183885@qq.com> Date: Thu, 14 May 2026 13:38:17 +0800 Subject: [PATCH 3/6] fix(vscode-ide-companion): remove dead ensureLeftGroupOfChatWebview, fix param naming, add tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Delete ensureLeftGroupOfChatWebview and waitForTabGroupsCondition which are no longer called by any code path - Remove now-unused openChatCommand import - Rename _cur → cur in findNeighborGroup callbacks (param was used, the underscore prefix was misleading) - Add editorGroupUtils.test.ts with 12 unit tests for findLeft/findRight - Add createAndOpenTempFile viewColumn tests to FileMessageHandler.test.ts covering left-neighbor, right-neighbor, and Beside fallback cases Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../vscode-ide-companion/src/diff-manager.ts | 7 +- .../src/utils/editorGroupUtils.test.ts | 175 ++++++++++++++++++ .../src/utils/editorGroupUtils.ts | 102 +--------- .../handlers/FileMessageHandler.test.ts | 114 ++++++++++++ 4 files changed, 293 insertions(+), 105 deletions(-) create mode 100644 packages/vscode-ide-companion/src/utils/editorGroupUtils.test.ts diff --git a/packages/vscode-ide-companion/src/diff-manager.ts b/packages/vscode-ide-companion/src/diff-manager.ts index af4edcc9472..7fc55c3fd0b 100644 --- a/packages/vscode-ide-companion/src/diff-manager.ts +++ b/packages/vscode-ide-companion/src/diff-manager.ts @@ -225,7 +225,7 @@ export class DiffManager { // Prefer opening the diff in the group to the left of the chat webview. // When that isn't available (e.g. chat is in the leftmost group), try the // group to the right so we reuse existing layout. Only fall back to - // ViewColumn.Beside when neither neighbour exists. + // ViewColumn.Beside when neither neighbor exists. const targetViewColumn = findLeftGroupOfChatWebview() ?? findRightGroupOfChatWebview() ?? @@ -237,10 +237,7 @@ export class DiffManager { rightDocUri, diffTitle, { - // If a left-of-webview group was found, target it explicitly so the - // diff opens there while keeping focus on the webview. Otherwise, use - // the default "open to side" behavior. - viewColumn: targetViewColumn ?? vscode.ViewColumn.Beside, + viewColumn: targetViewColumn, preview: false, preserveFocus: true, }, diff --git a/packages/vscode-ide-companion/src/utils/editorGroupUtils.test.ts b/packages/vscode-ide-companion/src/utils/editorGroupUtils.test.ts new file mode 100644 index 00000000000..a8dd4ce38f5 --- /dev/null +++ b/packages/vscode-ide-companion/src/utils/editorGroupUtils.test.ts @@ -0,0 +1,175 @@ +/** + * @license + * Copyright 2025 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + */ + +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +const chatViewType = 'mainThreadWebview-qwenCode.chat'; + +const vscodeMock = vi.hoisted(() => ({ + ViewColumn: { One: 1, Two: 2, Three: 3, Four: 4 }, + window: { + tabGroups: { + all: [] as Array<{ tabs: Array<{ input: unknown }>; viewColumn: number }>, + }, + }, +})); + +vi.mock('vscode', () => vscodeMock); + +import { + findLeftGroupOfChatWebview, + findRightGroupOfChatWebview, +} from './editorGroupUtils.js'; + +function chatTab() { + return { input: { viewType: chatViewType } }; +} + +function regularTab() { + return { input: { viewType: 'default' } }; +} + +describe('findLeftGroupOfChatWebview', () => { + beforeEach(() => { + vi.clearAllMocks(); + vscodeMock.window.tabGroups.all = []; + }); + + it('returns the nearest left neighbor when chat webview has a group to its left', () => { + vscodeMock.window.tabGroups.all = [ + { tabs: [regularTab()], viewColumn: 1 }, + { tabs: [chatTab()], viewColumn: 2 }, + { tabs: [regularTab()], viewColumn: 3 }, + ]; + + expect(findLeftGroupOfChatWebview()).toBe(1); + }); + + it('returns the closest left neighbor when multiple left groups exist', () => { + vscodeMock.window.tabGroups.all = [ + { tabs: [regularTab()], viewColumn: 1 }, + { tabs: [regularTab()], viewColumn: 2 }, + { tabs: [chatTab()], viewColumn: 4 }, + { tabs: [regularTab()], viewColumn: 5 }, + ]; + + // closest left is group 2, not group 1 + expect(findLeftGroupOfChatWebview()).toBe(2); + }); + + it('returns undefined when chat webview is in the leftmost group', () => { + vscodeMock.window.tabGroups.all = [ + { tabs: [chatTab()], viewColumn: 1 }, + { tabs: [regularTab()], viewColumn: 2 }, + ]; + + expect(findLeftGroupOfChatWebview()).toBeUndefined(); + }); + + it('returns undefined when no chat webview is found', () => { + vscodeMock.window.tabGroups.all = [ + { tabs: [regularTab()], viewColumn: 1 }, + { tabs: [regularTab()], viewColumn: 2 }, + ]; + + expect(findLeftGroupOfChatWebview()).toBeUndefined(); + }); + + it('returns undefined when there are no tab groups', () => { + vscodeMock.window.tabGroups.all = []; + + expect(findLeftGroupOfChatWebview()).toBeUndefined(); + }); + + it('returns undefined when tabGroups access throws', () => { + // make .all throw on access + Object.defineProperty(vscodeMock.window.tabGroups, 'all', { + get: () => { + throw new Error('unexpected error'); + }, + configurable: true, + }); + + expect(findLeftGroupOfChatWebview()).toBeUndefined(); + + // restore + Object.defineProperty(vscodeMock.window.tabGroups, 'all', { + value: [], + configurable: true, + writable: true, + }); + }); +}); + +describe('findRightGroupOfChatWebview', () => { + beforeEach(() => { + vi.clearAllMocks(); + vscodeMock.window.tabGroups.all = []; + }); + + it('returns the nearest right neighbor when chat webview has a group to its right', () => { + vscodeMock.window.tabGroups.all = [ + { tabs: [regularTab()], viewColumn: 1 }, + { tabs: [chatTab()], viewColumn: 2 }, + { tabs: [regularTab()], viewColumn: 3 }, + ]; + + expect(findRightGroupOfChatWebview()).toBe(3); + }); + + it('returns the closest right neighbor when multiple right groups exist', () => { + vscodeMock.window.tabGroups.all = [ + { tabs: [regularTab()], viewColumn: 1 }, + { tabs: [chatTab()], viewColumn: 2 }, + { tabs: [regularTab()], viewColumn: 3 }, + { tabs: [regularTab()], viewColumn: 5 }, + ]; + + // closest right is group 3, not group 5 + expect(findRightGroupOfChatWebview()).toBe(3); + }); + + it('returns undefined when chat webview is in the rightmost group', () => { + vscodeMock.window.tabGroups.all = [ + { tabs: [regularTab()], viewColumn: 1 }, + { tabs: [chatTab()], viewColumn: 3 }, + ]; + + expect(findRightGroupOfChatWebview()).toBeUndefined(); + }); + + it('returns undefined when no chat webview is found', () => { + vscodeMock.window.tabGroups.all = [ + { tabs: [regularTab()], viewColumn: 1 }, + { tabs: [regularTab()], viewColumn: 2 }, + ]; + + expect(findRightGroupOfChatWebview()).toBeUndefined(); + }); + + it('returns undefined when there are no tab groups', () => { + vscodeMock.window.tabGroups.all = []; + + expect(findRightGroupOfChatWebview()).toBeUndefined(); + }); + + it('returns undefined when tabGroups access throws', () => { + Object.defineProperty(vscodeMock.window.tabGroups, 'all', { + get: () => { + throw new Error('unexpected error'); + }, + configurable: true, + }); + + expect(findRightGroupOfChatWebview()).toBeUndefined(); + + Object.defineProperty(vscodeMock.window.tabGroups, 'all', { + value: [], + configurable: true, + writable: true, + }); + }); +}); diff --git a/packages/vscode-ide-companion/src/utils/editorGroupUtils.ts b/packages/vscode-ide-companion/src/utils/editorGroupUtils.ts index 32badcce9f1..3326cd33683 100644 --- a/packages/vscode-ide-companion/src/utils/editorGroupUtils.ts +++ b/packages/vscode-ide-companion/src/utils/editorGroupUtils.ts @@ -5,7 +5,6 @@ */ import * as vscode from 'vscode'; -import { openChatCommand } from '../commands/index.js'; const CHAT_WEBVIEW_TYPE = 'mainThreadWebview-qwenCode.chat'; @@ -53,7 +52,7 @@ export function findLeftGroupOfChatWebview(): vscode.ViewColumn | undefined { // Among groups with smaller viewColumn, pick the largest (closest neighbor). return findNeighborGroup( (v) => v < webviewGroup.viewColumn, - (_cur, cand) => cand > _cur, + (cur, cand) => cand > cur, ); } catch (_err) { return undefined; @@ -75,106 +74,9 @@ export function findRightGroupOfChatWebview(): vscode.ViewColumn | undefined { // Among groups with larger viewColumn, pick the smallest (closest neighbor). return findNeighborGroup( (v) => v > webviewGroup.viewColumn, - (_cur, cand) => cand < _cur, + (cur, cand) => cand < cur, ); } catch (_err) { return undefined; } } - -/** - * Wait for a condition to become true, driven by tab-group change events. - * Falls back to a timeout to avoid hanging forever. - */ -function waitForTabGroupsCondition( - condition: () => boolean, - timeout: number = 2000, -): Promise { - if (condition()) { - return Promise.resolve(true); - } - - return new Promise((resolve) => { - const subscription = vscode.window.tabGroups.onDidChangeTabGroups(() => { - if (!condition()) { - return; - } - clearTimeout(timeoutHandle); - subscription.dispose(); - resolve(true); - }); - - const timeoutHandle = setTimeout(() => { - subscription.dispose(); - resolve(false); - }, timeout); - }); -} - -/** - * Ensure there is an editor group directly to the left of the Qwen chat webview. - * - If one exists, return its ViewColumn. - * - If none exists, focus the chat panel and create a new group on its left, - * then return the new group's ViewColumn. - * - If the chat webview cannot be located, returns undefined. - */ -export async function ensureLeftGroupOfChatWebview(): Promise< - vscode.ViewColumn | undefined -> { - // First try to find an existing left neighbor - const existing = findLeftGroupOfChatWebview(); - if (existing !== undefined) { - return existing; - } - - // Locate the chat webview group - const webviewGroup = findWebviewGroup(); - - if (!webviewGroup) { - return undefined; - } - - const initialGroupCount = vscode.window.tabGroups.all.length; - - // Make the chat group active by revealing the panel - try { - await vscode.commands.executeCommand(openChatCommand); - } catch { - // Best-effort; continue even if this fails - } - - // Create a new group to the left of the chat group - try { - await vscode.commands.executeCommand('workbench.action.newGroupLeft'); - } catch { - // If we fail to create a group, fall back to default behavior - return undefined; - } - - // Wait for the new group to actually be created (check that group count increased) - const groupCreated = await waitForTabGroupsCondition( - () => vscode.window.tabGroups.all.length > initialGroupCount, - 1000, // 1 second timeout - ); - - if (!groupCreated) { - // Fallback if group creation didn't complete in time - return vscode.ViewColumn.One; - } - - // After creating a new group to the left, the new group takes ViewColumn.One - // and all existing groups shift right. So the new left group is always ViewColumn.One. - // However, to be safe, let's query for it again. - const newLeftGroup = findLeftGroupOfChatWebview(); - - // Restore focus to chat (optional), so we don't disturb user focus - try { - await vscode.commands.executeCommand(openChatCommand); - } catch { - // Ignore - } - - // If we successfully found the new left group, return it - // Otherwise, fallback to ViewColumn.One (the newly created group should be first) - return newLeftGroup ?? vscode.ViewColumn.One; -} diff --git a/packages/vscode-ide-companion/src/webview/handlers/FileMessageHandler.test.ts b/packages/vscode-ide-companion/src/webview/handlers/FileMessageHandler.test.ts index d6ff4c4a9f5..f7d59c8990d 100644 --- a/packages/vscode-ide-companion/src/webview/handlers/FileMessageHandler.test.ts +++ b/packages/vscode-ide-companion/src/webview/handlers/FileMessageHandler.test.ts @@ -32,10 +32,12 @@ const vscodeMock = vi.hoisted(() => { return { Uri, + ViewColumn: { One: 1, Two: 2, Three: 3, Beside: -2 }, workspace: { findFiles: vi.fn(), getWorkspaceFolder: vi.fn(), asRelativePath: vi.fn(), + openTextDocument: vi.fn(), workspaceFolders: [] as vscode.WorkspaceFolder[], createFileSystemWatcher: vi.fn(() => ({ onDidCreate: vi.fn(), @@ -47,6 +49,7 @@ const vscodeMock = vi.hoisted(() => { }, window: { activeTextEditor: undefined, + showTextDocument: vi.fn(), tabGroups: { all: [], }, @@ -74,6 +77,15 @@ vi.mock('@qwen-code/qwen-code-core/src/utils/filesearch/crawlCache.js', () => ({ clear: vi.fn(), })); +const readonlyProviderMock = vi.hoisted(() => ({ + createUri: vi.fn(), + setContent: vi.fn(), + getInstance: vi.fn(), +})); +vi.mock('../../services/readonlyFileSystemProvider.js', () => ({ + ReadonlyFileSystemProvider: readonlyProviderMock, +})); + describe('FileMessageHandler', () => { beforeEach(() => { vi.clearAllMocks(); @@ -183,4 +195,106 @@ describe('FileMessageHandler', () => { expect(payload.type).toBe('workspaceFiles'); expect(payload.data.requestId).toBe(7); }); + + describe('createAndOpenTempFile viewColumn selection', () => { + const chatViewType = 'mainThreadWebview-qwenCode.chat'; + + beforeEach(() => { + vi.clearAllMocks(); + readonlyProviderMock.getInstance.mockReturnValue(readonlyProviderMock); + readonlyProviderMock.createUri.mockReturnValue( + vscodeMock.Uri.file('/tmp/temp.txt'), + ); + readonlyProviderMock.setContent.mockReturnValue(undefined); + vscodeMock.workspace.openTextDocument.mockResolvedValue({ + uri: vscodeMock.Uri.file('/tmp/temp.txt'), + }); + vscodeMock.window.showTextDocument.mockResolvedValue(undefined); + // ensure the existing-tab search finds nothing + vscodeMock.window.tabGroups.all = []; + }); + + function chatTab() { + return { input: { viewType: chatViewType as unknown } }; + } + + function regularTab() { + return { input: { viewType: 'default' as unknown } }; + } + + it('opens in left group when chat webview has a left neighbor', async () => { + vscodeMock.window.tabGroups.all = [ + { tabs: [regularTab()], viewColumn: 1 }, + { tabs: [chatTab()], viewColumn: 2 }, + ]; + + const sendToWebView = vi.fn(); + const handler = new FileMessageHandler( + {} as QwenAgentManager, + {} as ConversationStore, + null, + sendToWebView, + ); + + await handler.handle({ + type: 'createAndOpenTempFile' as never, + data: { content: 'hello', fileName: 'test.txt' }, + }); + + expect(vscodeMock.window.showTextDocument).toHaveBeenCalledTimes(1); + const options = vscodeMock.window.showTextDocument.mock.calls[0]?.[1] as { + viewColumn: number; + }; + expect(options.viewColumn).toBe(1); + }); + + it('opens in right group when no left neighbor but right exists', async () => { + vscodeMock.window.tabGroups.all = [ + { tabs: [chatTab()], viewColumn: 1 }, + { tabs: [regularTab()], viewColumn: 2 }, + ]; + + const sendToWebView = vi.fn(); + const handler = new FileMessageHandler( + {} as QwenAgentManager, + {} as ConversationStore, + null, + sendToWebView, + ); + + await handler.handle({ + type: 'createAndOpenTempFile' as never, + data: { content: 'hello', fileName: 'test.txt' }, + }); + + expect(vscodeMock.window.showTextDocument).toHaveBeenCalledTimes(1); + const options = vscodeMock.window.showTextDocument.mock.calls[0]?.[1] as { + viewColumn: number; + }; + expect(options.viewColumn).toBe(2); + }); + + it('falls back to ViewColumn.Beside when neither left nor right neighbor exists', async () => { + vscodeMock.window.tabGroups.all = [{ tabs: [chatTab()], viewColumn: 1 }]; + + const sendToWebView = vi.fn(); + const handler = new FileMessageHandler( + {} as QwenAgentManager, + {} as ConversationStore, + null, + sendToWebView, + ); + + await handler.handle({ + type: 'createAndOpenTempFile' as never, + data: { content: 'hello', fileName: 'test.txt' }, + }); + + expect(vscodeMock.window.showTextDocument).toHaveBeenCalledTimes(1); + const options = vscodeMock.window.showTextDocument.mock.calls[0]?.[1] as { + viewColumn: number; + }; + expect(options.viewColumn).toBe(vscodeMock.ViewColumn.Beside); + }); + }); }); From cab669f8b971dc0809f3ccb3fd86933e3ad3e9f9 Mon Sep 17 00:00:00 2001 From: yiliang114 <1204183885@qq.com> Date: Thu, 14 May 2026 13:47:31 +0800 Subject: [PATCH 4/6] fix(vscode-ide-companion): align Beside fallback placement in FileMessageHandler with diff-manager Move vscode.ViewColumn.Beside fallback to the targetViewColumn declaration so both diff-manager and FileMessageHandler follow the same pattern: left ?? right ?? Beside at declaration, plain viewColumn at usage. --- .../src/webview/handlers/FileMessageHandler.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/vscode-ide-companion/src/webview/handlers/FileMessageHandler.ts b/packages/vscode-ide-companion/src/webview/handlers/FileMessageHandler.ts index 1a8714484e2..ce6090177f5 100644 --- a/packages/vscode-ide-companion/src/webview/handlers/FileMessageHandler.ts +++ b/packages/vscode-ide-companion/src/webview/handlers/FileMessageHandler.ts @@ -673,14 +673,17 @@ export class FileMessageHandler extends BaseMessageHandler { return; } - // Find the nearest editor group to the left or right of the chat webview + // Find the nearest editor group to the left or right of the chat webview. + // Fall back to ViewColumn.Beside when neither neighbor exists. const targetViewColumn = - findLeftGroupOfChatWebview() ?? findRightGroupOfChatWebview(); + findLeftGroupOfChatWebview() ?? + findRightGroupOfChatWebview() ?? + vscode.ViewColumn.Beside; // Open as readonly document in the selected neighboring group and focus it (single click should be enough) const document = await vscode.workspace.openTextDocument(uri); await vscode.window.showTextDocument(document, { - viewColumn: targetViewColumn ?? vscode.ViewColumn.Beside, + viewColumn: targetViewColumn, preview: false, preserveFocus: false, }); From e00505fdc6561a61e6c91c44c81883d416067cbb Mon Sep 17 00:00:00 2001 From: yiliang114 <1204183885@qq.com> Date: Thu, 14 May 2026 13:54:18 +0800 Subject: [PATCH 5/6] fix(vscode-ide-companion): fix TS2322 type error in FileMessageHandler.test.ts vscodeMock.window.tabGroups.all was initialized as plain [] which TypeScript infers as never[], causing assignment errors in CI. Add explicit type annotation to match the objects assigned in tests. --- .../src/webview/handlers/FileMessageHandler.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/vscode-ide-companion/src/webview/handlers/FileMessageHandler.test.ts b/packages/vscode-ide-companion/src/webview/handlers/FileMessageHandler.test.ts index f7d59c8990d..faeaa8f19f7 100644 --- a/packages/vscode-ide-companion/src/webview/handlers/FileMessageHandler.test.ts +++ b/packages/vscode-ide-companion/src/webview/handlers/FileMessageHandler.test.ts @@ -51,7 +51,10 @@ const vscodeMock = vi.hoisted(() => { activeTextEditor: undefined, showTextDocument: vi.fn(), tabGroups: { - all: [], + all: [] as Array<{ + tabs: Array<{ input: unknown }>; + viewColumn: number; + }>, }, }, }; From 680b0166439520e1b5bc28d06e75404716b33d9c Mon Sep 17 00:00:00 2001 From: yiliang114 <1204183885@qq.com> Date: Thu, 14 May 2026 15:24:45 +0800 Subject: [PATCH 6/6] =?UTF-8?q?fix(vscode-ide-companion):=20clarify=20Besi?= =?UTF-8?q?de=20fallback=20comment=20=E2=80=94=20covers=20missing=20webvie?= =?UTF-8?q?w=20too?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The fallback chain left ?? right ?? Beside also falls through to Beside when the chat webview group is not found (both helpers return undefined). Update comments in both diff-manager and FileMessageHandler. --- packages/vscode-ide-companion/src/diff-manager.ts | 2 +- .../src/webview/handlers/FileMessageHandler.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/vscode-ide-companion/src/diff-manager.ts b/packages/vscode-ide-companion/src/diff-manager.ts index 7fc55c3fd0b..755143a4e44 100644 --- a/packages/vscode-ide-companion/src/diff-manager.ts +++ b/packages/vscode-ide-companion/src/diff-manager.ts @@ -225,7 +225,7 @@ export class DiffManager { // Prefer opening the diff in the group to the left of the chat webview. // When that isn't available (e.g. chat is in the leftmost group), try the // group to the right so we reuse existing layout. Only fall back to - // ViewColumn.Beside when neither neighbor exists. + // ViewColumn.Beside when neither neighbor exists or the webview is missing. const targetViewColumn = findLeftGroupOfChatWebview() ?? findRightGroupOfChatWebview() ?? diff --git a/packages/vscode-ide-companion/src/webview/handlers/FileMessageHandler.ts b/packages/vscode-ide-companion/src/webview/handlers/FileMessageHandler.ts index ce6090177f5..547cd6108a6 100644 --- a/packages/vscode-ide-companion/src/webview/handlers/FileMessageHandler.ts +++ b/packages/vscode-ide-companion/src/webview/handlers/FileMessageHandler.ts @@ -674,7 +674,7 @@ export class FileMessageHandler extends BaseMessageHandler { } // Find the nearest editor group to the left or right of the chat webview. - // Fall back to ViewColumn.Beside when neither neighbor exists. + // Fall back to ViewColumn.Beside when neither neighbor exists or the webview is missing. const targetViewColumn = findLeftGroupOfChatWebview() ?? findRightGroupOfChatWebview() ??