From de7441554440919f68017f63d4f73327cc512697 Mon Sep 17 00:00:00 2001 From: Benjamin Liu Date: Wed, 27 May 2026 08:00:24 +0900 Subject: [PATCH] fix(extension): skip user windows in tab-group discovery (fixes #1760) Chrome can move our "OpenCLI Adapter" tab group into the user's main window via drag or window merge. After the original automation window closes the residual group survives in the user's window, and the next service-worker discovery cycle adopts that window as the automation container, letting `resolveTab` reuse the user's real http(s) tabs as automation targets. `toOwnedContainerDiscoveryCandidate` now skips any candidate window whose tabs include http(s) pages outside the matched group, since extension-created adapter tabs are always inside the group. Fixes #1760 --- extension/dist/background.js | 5 +++++ extension/src/background.test.ts | 29 +++++++++++++++++++++++++++++ extension/src/background.ts | 7 +++++++ 3 files changed, 41 insertions(+) diff --git a/extension/dist/background.js b/extension/dist/background.js index f456097fa..47117fd0a 100644 --- a/extension/dist/background.js +++ b/extension/dist/background.js @@ -1045,6 +1045,11 @@ async function focusOwnedWindowIfRequested(windowId, mode) { async function toOwnedContainerDiscoveryCandidate(group) { try { const chromeWindow = await chrome.windows.get(group.windowId); + const windowTabs = await chrome.tabs.query({ windowId: group.windowId }); + const hasUserTabsOutsideGroup = windowTabs.some( + (tab) => tab.groupId !== group.id && !!tab.url && isSafeNavigationUrl(tab.url) + ); + if (hasUserTabsOutsideGroup) return null; const reusableTabId = await findReusableOwnedContainerTab(group.windowId); return { windowId: group.windowId, diff --git a/extension/src/background.test.ts b/extension/src/background.test.ts index 510da347e..35c2ede5b 100644 --- a/extension/src/background.test.ts +++ b/extension/src/background.test.ts @@ -1290,6 +1290,35 @@ describe('background tab isolation', () => { expect(chrome.tabGroups.update).not.toHaveBeenCalled(); }); + it('skips a residual OpenCLI Adapter group when its window holds user http tabs outside the group', async () => { + const { chrome, tabs, groups } = createChromeMock(); + tabs.push({ + id: 77, + windowId: 7, + url: 'https://example.com/users-real-tab', + title: 'users real tab', + active: true, + status: 'complete', + groupId: -1, + }); + groups.push({ + id: 99, + windowId: 7, + title: 'OpenCLI Adapter', + color: 'orange', + collapsed: true, + }); + vi.stubGlobal('chrome', chrome); + + const mod = await import('./background'); + const tabId = await mod.__test__.resolveTabId(undefined, adapterKey('twitter')); + + expect(chrome.windows.create).toHaveBeenCalledTimes(1); + expect(mod.__test__.getAutomationWindowId(adapterKey('twitter'))).not.toBe(7); + expect(tabs.find((tab) => tab.id === 77)?.url).toBe('https://example.com/users-real-tab'); + expect(tabs.find((tab) => tab.id === tabId)?.windowId).not.toBe(7); + }); + it('prefers a focused OpenCLI Adapter group when multiple matching groups exist', async () => { const { chrome, tabs, groups, setLastFocusedWindowId } = createChromeMock(); setLastFocusedWindowId(8); diff --git a/extension/src/background.ts b/extension/src/background.ts index 7ec54dbb2..931741f28 100644 --- a/extension/src/background.ts +++ b/extension/src/background.ts @@ -557,6 +557,13 @@ async function focusOwnedWindowIfRequested(windowId: number, mode: WindowMode): async function toOwnedContainerDiscoveryCandidate(group: chrome.tabGroups.TabGroup): Promise { try { const chromeWindow = await chrome.windows.get(group.windowId); + // Skip windows holding user http(s) tabs outside our group: a tab group + // moved into the user's main window (via drag or merge) is residue. + const windowTabs = await chrome.tabs.query({ windowId: group.windowId }); + const hasUserTabsOutsideGroup = windowTabs.some((tab) => + tab.groupId !== group.id && !!tab.url && isSafeNavigationUrl(tab.url), + ); + if (hasUserTabsOutsideGroup) return null; const reusableTabId = await findReusableOwnedContainerTab(group.windowId); return { windowId: group.windowId,