From 8475f82a9c7e43ad6e6054c4f85620fa4039d28b Mon Sep 17 00:00:00 2001 From: keen0206 Date: Mon, 11 May 2026 07:28:52 +0800 Subject: [PATCH] feat(extension): add opt-out flag to disable automation tab grouping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Chrome 119+ ships a "Saved Tab Groups" feature that auto-saves any named/ colored tab group to the user's sidebar and keeps it there even after the underlying window is closed. For automation-heavy workflows that spin up many short-lived owned windows, this causes `OpenCLI Adapter` (or `OpenCLI Browser`) entries to accumulate in the saved-groups list, and neither the extension nor `chrome.tabGroups.*` exposes an API to remove them. This change adds a chrome.storage.local boolean `opencli_disable_automation_tab_group`. When set to `true`, `ensureOwnedContainerTabGroup` is a no-op — tabs are tracked entirely by id via the existing automationSessions map, so adapters keep working; only the visual grouping is suppressed. The default behavior is unchanged. Existing users see no difference. Users affected by saved-group accumulation can opt out from the extension's service worker DevTools console: chrome.storage.local.set({ opencli_disable_automation_tab_group: true }) Includes a unit test covering the opt-out path and documentation in extension/README.md. --- extension/README.md | 26 ++++++++++++++++++++++++++ extension/dist/background.js | 15 ++++++++++++++- extension/src/background.test.ts | 17 +++++++++++++++++ extension/src/background.ts | 18 ++++++++++++++++++ 4 files changed, 75 insertions(+), 1 deletion(-) diff --git a/extension/README.md b/extension/README.md index 4073eba11..45621e157 100644 --- a/extension/README.md +++ b/extension/README.md @@ -24,3 +24,29 @@ Suggested Chrome Web Store justification for `downloads`: > so agents can wait for downloads triggered during an automation workflow. The > command filters by a user-provided filename or URL pattern and timeout. We do > not modify, redirect, or persist user download history. + +## Configuration + +### `opencli_disable_automation_tab_group` (chrome.storage.local, boolean) + +When set to `true`, the extension stops placing owned tabs into a named tab +group (`OpenCLI Browser` / `OpenCLI Adapter`). Tabs are still tracked +internally by id, so adapters keep working — only the visual grouping is +suppressed. + +Why this exists: Chrome 119+ ships a "Saved Tab Groups" feature that +auto-saves any named/colored tab group to the user's sidebar and keeps it +there even after the underlying window is closed. For automation-heavy +workflows that spin up many short-lived owned windows, this causes +`OpenCLI Adapter` (or `OpenCLI Browser`) entries to accumulate. The flag +lets users opt out. + +To enable, open the extension's service worker DevTools console +(`chrome://extensions` → OpenCLI → "Inspect views: service worker") and run: + +```js +chrome.storage.local.set({ opencli_disable_automation_tab_group: true }); +``` + +To revert, set the same key to `false` (or remove it) and reload the +extension. The default behavior (tab grouping enabled) is unchanged. diff --git a/extension/dist/background.js b/extension/dist/background.js index c3cd0a4ac..016e0c509 100644 --- a/extension/dist/background.js +++ b/extension/dist/background.js @@ -755,6 +755,7 @@ const IDLE_TIMEOUT_INTERACTIVE = 6e5; const IDLE_TIMEOUT_NONE = -1; const REGISTRY_KEY = "opencli_target_lease_registry_v2"; const LEASE_IDLE_ALARM_PREFIX = "opencli:lease-idle:"; +const DISABLE_TAB_GROUP_KEY = "opencli_disable_automation_tab_group"; const CONTAINER_TAB_GROUP_TITLE = { interactive: "OpenCLI Browser", automation: "OpenCLI Adapter" @@ -810,7 +811,8 @@ function getSessionFromKey(key) { function getIdleTimeout(key) { const session = automationSessions.get(key); if (session?.kind === "bound") return IDLE_TIMEOUT_NONE; - if (getSurfaceFromKey(key) === "adapter" && (session?.lifecycle === "persistent" || sessionLifecycleOverrides.get(key) === "persistent")) return IDLE_TIMEOUT_NONE; + const adapterPersistent = getSurfaceFromKey(key) === "adapter" && (session?.lifecycle === "persistent" || sessionLifecycleOverrides.get(key) === "persistent"); + if (adapterPersistent) return IDLE_TIMEOUT_NONE; const override = sessionTimeoutOverrides.get(key); if (override !== void 0) return override; return getSurfaceFromKey(key) === "browser" ? IDLE_TIMEOUT_INTERACTIVE : IDLE_TIMEOUT_DEFAULT; @@ -1002,9 +1004,20 @@ async function getOwnedContainerGroupId(role, windowId) { container.groupId = existing.id; return existing.id; } +async function isAutomationTabGroupDisabled() { + try { + const local = chrome.storage?.local; + if (!local) return false; + const raw = await local.get(DISABLE_TAB_GROUP_KEY); + return raw[DISABLE_TAB_GROUP_KEY] === true; + } catch { + return false; + } +} async function ensureOwnedContainerTabGroup(role, windowId, tabIds) { const ids = [...new Set(tabIds.filter((id) => id !== void 0))]; if (ids.length === 0) return; + if (await isAutomationTabGroupDisabled()) return; try { const existingGroupId = await getOwnedContainerGroupId(role, windowId); if (existingGroupId !== null) { diff --git a/extension/src/background.test.ts b/extension/src/background.test.ts index 81181d4b0..0fa54fb69 100644 --- a/extension/src/background.test.ts +++ b/extension/src/background.test.ts @@ -901,6 +901,23 @@ describe('background tab isolation', () => { expect(chrome.tabs.group).toHaveBeenCalledWith({ tabIds: [1], createProperties: { windowId: 1 } }); }); + it('skips tab grouping when opencli_disable_automation_tab_group is set in storage', async () => { + const { chrome, tabs, groups } = createChromeMock(); + // Pre-set the opt-out flag before background.ts loads. + await chrome.storage.local.set({ opencli_disable_automation_tab_group: true }); + vi.stubGlobal('chrome', chrome); + + const mod = await import('./background'); + const tabId = await mod.__test__.resolveTabId(undefined, adapterKey('twitter')); + + // The tab is still created and resolvable — only the visual grouping is suppressed. + expect(tabId).toBe(1); + expect(tabs[0].groupId).toBe(-1); + expect(groups).toHaveLength(0); + expect(chrome.tabs.group).not.toHaveBeenCalled(); + expect(chrome.tabGroups.update).not.toHaveBeenCalled(); + }); + it('uses separate owned windows for browser and adapter sessions', async () => { const { chrome, tabs, groups } = createChromeMock(); let nextWindowId = 20; diff --git a/extension/src/background.ts b/extension/src/background.ts index 1fbd87771..e35492cff 100644 --- a/extension/src/background.ts +++ b/extension/src/background.ts @@ -200,6 +200,12 @@ const IDLE_TIMEOUT_INTERACTIVE = 600_000; // 10min — human-paced browser:* / o const IDLE_TIMEOUT_NONE = -1; // borrowed bound tabs stay bound until unbound/closed const REGISTRY_KEY = 'opencli_target_lease_registry_v2'; const LEASE_IDLE_ALARM_PREFIX = 'opencli:lease-idle:'; +// Storage flag (chrome.storage.local) — when set to `true`, the extension stops +// placing owned tabs into a named tab group. Useful on Chrome 119+ where the +// "Saved Tab Groups" feature persists every named group's metadata across +// sessions, causing OpenCLI entries to accumulate in the user's sidebar for +// automation-heavy workflows. +const DISABLE_TAB_GROUP_KEY = 'opencli_disable_automation_tab_group'; const CONTAINER_TAB_GROUP_TITLE: Record = { interactive: 'OpenCLI Browser', automation: 'OpenCLI Adapter', @@ -501,9 +507,21 @@ async function getOwnedContainerGroupId(role: OwnedWindowRole, windowId: number) return existing.id; } +async function isAutomationTabGroupDisabled(): Promise { + try { + const local = chrome.storage?.local; + if (!local) return false; + const raw = await local.get(DISABLE_TAB_GROUP_KEY) as Record; + return raw[DISABLE_TAB_GROUP_KEY] === true; + } catch { + return false; + } +} + async function ensureOwnedContainerTabGroup(role: OwnedWindowRole, windowId: number, tabIds: Array): Promise { const ids = [...new Set(tabIds.filter((id): id is number => id !== undefined))]; if (ids.length === 0) return; + if (await isAutomationTabGroupDisabled()) return; try { const existingGroupId = await getOwnedContainerGroupId(role, windowId);