Skip to content
Merged
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
87 changes: 77 additions & 10 deletions extension/dist/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -759,6 +759,7 @@ const CONTAINER_TAB_GROUP_TITLE = {
interactive: "OpenCLI Browser",
automation: "OpenCLI Adapter"
};
const LEGACY_AUTOMATION_TAB_GROUP_TITLE = "OpenCLI";
const AUTOMATION_TAB_GROUP_COLOR = "orange";
let leaseMutationQueue = Promise.resolve();
const ownedContainers = {
Expand Down Expand Up @@ -994,11 +995,70 @@ async function getOwnedContainerGroupId(role, windowId) {
}
container.groupId = null;
}
const groups = await chrome.tabGroups.query({ windowId, title: CONTAINER_TAB_GROUP_TITLE[role] });
const existing = groups[0];
if (!existing) return null;
container.groupId = existing.id;
return existing.id;
for (const title of getOwnedContainerGroupTitles(role)) {
const groups = await chrome.tabGroups.query({ windowId, title });
const existing = groups[0];
if (existing) {
container.groupId = existing.id;
return existing.id;
}
}
return null;
}
function getOwnedContainerGroupTitles(role) {
return role === "automation" ? [CONTAINER_TAB_GROUP_TITLE.automation, LEGACY_AUTOMATION_TAB_GROUP_TITLE] : [CONTAINER_TAB_GROUP_TITLE.interactive];
}
async function focusOwnedWindowIfRequested(windowId, mode) {
if (mode !== "foreground") return;
const updateWindow = chrome.windows.update;
if (typeof updateWindow === "function") await updateWindow(windowId, { focused: true }).catch(() => {
});
}
async function toOwnedContainerDiscoveryCandidate(group) {
try {
const chromeWindow = await chrome.windows.get(group.windowId);
const reusableTabId = await findReusableOwnedContainerTab(group.windowId);
return {
windowId: group.windowId,
groupId: group.id,
focused: !!chromeWindow.focused,
hasReusableTab: reusableTabId !== void 0
};
} catch {
return null;
}
}
function selectOwnedContainerDiscoveryCandidate(candidates) {
if (candidates.length === 0) return null;
return [...candidates].sort((a, b) => {
if (a.focused !== b.focused) return a.focused ? -1 : 1;
if (a.hasReusableTab !== b.hasReusableTab) return a.hasReusableTab ? -1 : 1;
return a.groupId - b.groupId;
})[0];
}
async function discoverOwnedContainerFromTabGroup(role) {
const container = ownedContainers[role];
if (container.groupId !== null) {
try {
const group = await chrome.tabGroups.get(container.groupId);
await chrome.windows.get(group.windowId);
container.windowId = group.windowId;
return { windowId: group.windowId, groupId: group.id };
} catch {
container.windowId = null;
container.groupId = null;
}
}
for (const title of getOwnedContainerGroupTitles(role)) {
const groups = await chrome.tabGroups.query({ title });
const candidates = (await Promise.all(groups.map(toOwnedContainerDiscoveryCandidate))).filter((candidate) => candidate !== null);
const selected = selectOwnedContainerDiscoveryCandidate(candidates);
if (!selected) continue;
container.windowId = selected.windowId;
container.groupId = selected.groupId;
return { windowId: selected.windowId, groupId: selected.groupId };
}
return null;
}
async function ensureOwnedContainerTabGroup(role, windowId, tabIds) {
const ids = [...new Set(tabIds.filter((id) => id !== void 0))];
Expand Down Expand Up @@ -1038,11 +1098,7 @@ async function ensureOwnedContainerWindowUnlocked(role, initialUrl, mode = "back
if (container.windowId !== null) {
try {
await chrome.windows.get(container.windowId);
if (mode === "foreground") {
const updateWindow = chrome.windows.update;
if (typeof updateWindow === "function") await updateWindow(container.windowId, { focused: true }).catch(() => {
});
}
await focusOwnedWindowIfRequested(container.windowId, mode);
const initialTabId2 = await findReusableOwnedContainerTab(container.windowId);
await ensureOwnedContainerTabGroup(role, container.windowId, [initialTabId2]);
return {
Expand All @@ -1054,6 +1110,17 @@ async function ensureOwnedContainerWindowUnlocked(role, initialUrl, mode = "back
container.groupId = null;
}
}
const discovered = await discoverOwnedContainerFromTabGroup(role);
if (discovered) {
await focusOwnedWindowIfRequested(discovered.windowId, mode);
const initialTabId2 = await findReusableOwnedContainerTab(discovered.windowId);
await ensureOwnedContainerTabGroup(role, discovered.windowId, [initialTabId2]);
await persistRuntimeState();
return {
windowId: discovered.windowId,
initialTabId: initialTabId2
};
}
const startUrl = initialUrl && isSafeNavigationUrl(initialUrl) ? initialUrl : BLANK_PAGE;
const win = await chrome.windows.create({
url: startUrl,
Expand Down
174 changes: 171 additions & 3 deletions extension/src/background.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ function createChromeMock() {
onEvent: { addListener: vi.fn() } as Listener<(source: any, method: string, params: any) => void>,
},
windows: {
get: vi.fn(async (windowId: number) => ({ id: windowId })),
get: vi.fn(async (windowId: number) => ({ id: windowId, focused: windowId === lastFocusedWindowId })),
create: vi.fn(async ({ url, focused, width, height, type }: any) => ({ id: 1, url, focused, width, height, type })),
remove: vi.fn(async (_windowId: number) => {}),
onRemoved: { addListener: vi.fn() } as Listener<(windowId: number) => void>,
Expand Down Expand Up @@ -187,7 +187,15 @@ function createChromeMock() {
},
};

return { chrome, tabs, groups, query, create, update };
return {
chrome,
tabs,
groups,
query,
create,
update,
setLastFocusedWindowId: (windowId: number) => { lastFocusedWindowId = windowId; },
};
}

describe('background tab isolation', () => {
Expand Down Expand Up @@ -908,7 +916,7 @@ describe('background tab isolation', () => {
const { chrome } = createChromeMock();
chrome.windows.get = vi.fn(async (windowId: number) => {
if (windowId === 90 || windowId === 91) throw new Error(`stale window ${windowId}`);
return { id: windowId };
return { id: windowId, focused: false };
});
vi.stubGlobal('chrome', chrome);

Expand Down Expand Up @@ -1050,6 +1058,166 @@ describe('background tab isolation', () => {
expect(chrome.tabGroups.update).not.toHaveBeenCalled();
});

it('discovers and reuses an existing OpenCLI Adapter group in another window before creating one', async () => {
const { chrome, tabs, groups } = createChromeMock();
tabs.push({
id: 77,
windowId: 7,
url: 'about:blank',
title: 'blank',
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(tabId).toBe(77);
expect(chrome.windows.create).not.toHaveBeenCalled();
expect(mod.__test__.getAutomationWindowId(adapterKey('twitter'))).toBe(7);
expect(tabs.find((tab) => tab.id === 77)?.groupId).toBe(99);
expect(chrome.tabs.group).toHaveBeenCalledWith({ groupId: 99, tabIds: [77] });
expect(chrome.tabGroups.update).not.toHaveBeenCalled();
});

it('prefers a focused OpenCLI Adapter group when multiple matching groups exist', async () => {
const { chrome, tabs, groups, setLastFocusedWindowId } = createChromeMock();
setLastFocusedWindowId(8);
tabs.push({
id: 77,
windowId: 7,
url: 'about:blank',
title: 'blank',
active: true,
status: 'complete',
groupId: -1,
});
tabs.push({
id: 78,
windowId: 8,
url: 'about:blank',
title: 'blank',
active: true,
status: 'complete',
groupId: -1,
});
groups.push(
{
id: 99,
windowId: 7,
title: 'OpenCLI Adapter',
color: 'orange',
collapsed: true,
},
{
id: 98,
windowId: 8,
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(tabId).toBe(78);
expect(chrome.windows.create).not.toHaveBeenCalled();
expect(mod.__test__.getAutomationWindowId(adapterKey('twitter'))).toBe(8);
expect(tabs.find((tab) => tab.id === 78)?.groupId).toBe(98);
expect(chrome.tabs.group).toHaveBeenCalledWith({ groupId: 98, tabIds: [78] });
});

it('prefers an OpenCLI Adapter group with a reusable debuggable tab when none are focused', async () => {
const { chrome, tabs, groups, setLastFocusedWindowId } = createChromeMock();
setLastFocusedWindowId(2);
tabs.push({
id: 77,
windowId: 7,
url: 'chrome://settings',
title: 'settings',
active: true,
status: 'complete',
groupId: -1,
});
tabs.push({
id: 78,
windowId: 8,
url: 'about:blank',
title: 'blank',
active: true,
status: 'complete',
groupId: -1,
});
groups.push(
{
id: 97,
windowId: 7,
title: 'OpenCLI Adapter',
color: 'orange',
collapsed: true,
},
{
id: 98,
windowId: 8,
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(tabId).toBe(78);
expect(chrome.windows.create).not.toHaveBeenCalled();
expect(mod.__test__.getAutomationWindowId(adapterKey('twitter'))).toBe(8);
expect(tabs.find((tab) => tab.id === 78)?.groupId).toBe(98);
expect(chrome.tabs.group).toHaveBeenCalledWith({ groupId: 98, tabIds: [78] });
});

it('discovers and reuses a legacy OpenCLI automation group before creating a duplicate', async () => {
const { chrome, tabs, groups } = createChromeMock();
tabs.push({
id: 78,
windowId: 8,
url: 'about:blank',
title: 'blank',
active: true,
status: 'complete',
groupId: -1,
});
groups.push({
id: 98,
windowId: 8,
title: 'OpenCLI',
color: 'orange',
collapsed: true,
});
vi.stubGlobal('chrome', chrome);

const mod = await import('./background');
const tabId = await mod.__test__.resolveTabId(undefined, adapterKey('twitter'));

expect(tabId).toBe(78);
expect(chrome.windows.create).not.toHaveBeenCalled();
expect(mod.__test__.getAutomationWindowId(adapterKey('twitter'))).toBe(8);
expect(tabs.find((tab) => tab.id === 78)?.groupId).toBe(98);
expect(chrome.tabs.group).toHaveBeenCalledWith({ groupId: 98, tabIds: [78] });
expect(chrome.tabGroups.update).not.toHaveBeenCalled();
});

it('reuses a persisted automation group id after service worker restart even if the user renamed it', async () => {
const { chrome, tabs, groups } = createChromeMock();
groups.push({
Expand Down
Loading
Loading