diff --git a/extension/src/background.test.ts b/extension/src/background.test.ts index f9368251..f15964f1 100644 --- a/extension/src/background.test.ts +++ b/extension/src/background.test.ts @@ -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: { @@ -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/'; diff --git a/extension/src/background.ts b/extension/src/background.ts index a9d0439e..29766cd0 100644 --- a/extension/src/background.ts +++ b/extension/src/background.ts @@ -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`); } @@ -502,7 +513,22 @@ async function handleNavigate(cmd: Command, workspace: string): Promise }, 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, diff --git a/extension/src/cdp.ts b/extension/src/cdp.ts index 4e37179b..cbb8b2f7 100644 --- a/extension/src/cdp.ts +++ b/extension/src/cdp.ts @@ -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' : '';