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
39 changes: 39 additions & 0 deletions extension/src/background.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,12 @@ function createChromeMock() {
if (!tab) throw new Error(`Unknown tab ${tabId}`);
return tab;
}),
move: vi.fn(async (tabId: number, moveProps: { windowId: number; index: number }) => {
const tab = tabs.find((entry) => entry.id === tabId);
if (!tab) throw new Error(`Unknown tab ${tabId}`);
tab.windowId = moveProps.windowId;
return tab;
}),
onUpdated: { addListener: vi.fn(), removeListener: vi.fn() } as Listener<(id: number, info: chrome.tabs.TabChangeInfo) => void>,
},
windows: {
Expand Down Expand Up @@ -219,6 +225,39 @@ describe('background tab isolation', () => {
}));
});

it('moves drifted tab back to automation window instead of creating a new one', async () => {
const { chrome, tabs } = createChromeMock();
// Tab 1 belongs to automation window 1 but drifted to window 2
tabs[0].windowId = 2;
tabs[0].url = 'https://twitter.com/home';
vi.stubGlobal('chrome', chrome);

const mod = await import('./background');
mod.__test__.setAutomationWindowId('site:twitter', 1);

const tabId = await mod.__test__.resolveTabId(1, 'site:twitter');

// Should have moved tab 1 back to window 1 and reused it
expect(chrome.tabs.move).toHaveBeenCalledWith(1, { windowId: 1, index: -1 });
expect(tabId).toBe(1);
});

it('falls through to re-resolve when drifted tab move fails', async () => {
const { chrome, tabs } = createChromeMock();
tabs[0].windowId = 2;
tabs[0].url = 'https://twitter.com/home';
// Make move fail
chrome.tabs.move = vi.fn(async () => { throw new Error('Cannot move tab'); });
vi.stubGlobal('chrome', chrome);

const mod = await import('./background');
mod.__test__.setAutomationWindowId('site:twitter', 1);

// Should still resolve (by finding/creating a tab in the correct window)
const tabId = await mod.__test__.resolveTabId(1, 'site:twitter');
expect(typeof tabId).toBe('number');
});

it('idle timeout closes the automation window for site:notebooklm', async () => {
const { chrome, tabs } = createChromeMock();
tabs[0].url = 'https://notebooklm.google.com/';
Expand Down
32 changes: 29 additions & 3 deletions extension/src/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -350,8 +350,19 @@ async function resolveTab(tabId: number | undefined, workspace: string, initialU
const session = automationSessions.get(workspace);
const matchesSession = session ? tab.windowId === session.windowId : false;
if (isDebuggableUrl(tab.url) && matchesSession) return { tabId, tab };
if (session && !matchesSession) {
console.warn(`[opencli] Tab ${tabId} is not bound to workspace ${workspace}, re-resolving`);
if (session && !matchesSession && isDebuggableUrl(tab.url)) {
// Tab drifted to another window but content is still valid.
// Try to move it back instead of abandoning it.
console.warn(`[opencli] Tab ${tabId} drifted to window ${tab.windowId}, moving back to ${session.windowId}`);
try {
await chrome.tabs.move(tabId, { windowId: session.windowId, index: -1 });
const moved = await chrome.tabs.get(tabId);
if (moved.windowId === session.windowId && isDebuggableUrl(moved.url)) {
return { tabId, tab: moved };
}
} catch (moveErr) {
console.warn(`[opencli] Failed to move tab back: ${moveErr}`);
}
} else if (!isDebuggableUrl(tab.url)) {
console.warn(`[opencli] Tab ${tabId} URL is not debuggable (${tab.url}), re-resolving`);
}
Expand Down Expand Up @@ -502,7 +513,22 @@ async function handleNavigate(cmd: Command, workspace: string): Promise<Result>
}, 15000);
});

const tab = await chrome.tabs.get(tabId);
let tab = await chrome.tabs.get(tabId);

// Post-navigation drift detection: if the tab moved to another window
// during navigation (e.g. a tab-management extension regrouped it),
// try to move it back to maintain session isolation.
const session = automationSessions.get(workspace);
if (session && tab.windowId !== session.windowId) {
console.warn(`[opencli] Tab ${tabId} drifted to window ${tab.windowId} during navigation, moving back to ${session.windowId}`);
try {
await chrome.tabs.move(tabId, { windowId: session.windowId, index: -1 });
tab = await chrome.tabs.get(tabId);
} catch (moveErr) {
console.warn(`[opencli] Failed to recover drifted tab: ${moveErr}`);
}
}

return {
id: cmd.id,
ok: true,
Expand Down
10 changes: 10 additions & 0 deletions extension/src/cdp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,16 @@ export async function ensureAttached(tabId: number, aggressiveRetry: boolean = f
}

if (lastError) {
// Log detailed diagnostics for debugging extension conflicts
let finalUrl = 'unknown';
let finalWindowId = 'unknown';
try {
const tab = await chrome.tabs.get(tabId);
finalUrl = tab.url ?? 'undefined';
finalWindowId = String(tab.windowId);
} catch { /* tab gone */ }
console.warn(`[opencli] attach failed for tab ${tabId}: url=${finalUrl}, windowId=${finalWindowId}, error=${lastError}`);

const hint = lastError.includes('chrome-extension://')
? '. Tip: another Chrome extension may be interfering — try disabling other extensions'
: '';
Expand Down
Loading