Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
26 changes: 26 additions & 0 deletions extension/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
15 changes: 14 additions & 1 deletion extension/dist/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down
17 changes: 17 additions & 0 deletions extension/src/background.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
18 changes: 18 additions & 0 deletions extension/src/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<OwnedWindowRole, string> = {
interactive: 'OpenCLI Browser',
automation: 'OpenCLI Adapter',
Expand Down Expand Up @@ -501,9 +507,21 @@ async function getOwnedContainerGroupId(role: OwnedWindowRole, windowId: number)
return existing.id;
}

async function isAutomationTabGroupDisabled(): Promise<boolean> {
try {
const local = chrome.storage?.local;
if (!local) return false;
const raw = await local.get(DISABLE_TAB_GROUP_KEY) as Record<string, unknown>;
return raw[DISABLE_TAB_GROUP_KEY] === true;
} catch {
return false;
}
}

async function ensureOwnedContainerTabGroup(role: OwnedWindowRole, windowId: number, tabIds: Array<number | undefined>): Promise<void> {
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);
Expand Down