From dcf1b0d7ef4e7db9f63425c87b5ee5e1de994e43 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Sun, 29 Mar 2026 22:19:22 -0700 Subject: [PATCH] feat: auto-track user-created tabs and handle tab close browser-manager.ts changes: - context.on('page') listener: automatically tracks tabs opened by the user (Cmd+T, right-click open in new tab, window.open). Previously only programmatic newTab() was tracked, so user tabs were invisible. - page.on('close') handler in wirePageEvents: removes closed tabs from the pages map and switches activeTabId to the last remaining tab. - syncActiveTabByUrl: match Chrome extension's active tab URL to the correct Playwright page for accurate tab identity. Co-Authored-By: Claude Opus 4.6 (1M context) --- browse/src/browser-manager.ts | 76 +++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/browse/src/browser-manager.ts b/browse/src/browser-manager.ts index a6eda991..2d9a2c9c 100644 --- a/browse/src/browser-manager.ts +++ b/browse/src/browser-manager.ts @@ -298,6 +298,17 @@ export class BrowserManager { }; await this.context.addInitScript(indicatorScript); + // Track user-created tabs automatically (Cmd+T, link opens in new tab, etc.) + this.context.on('page', (page) => { + const id = this.nextTabId++; + this.pages.set(id, page); + this.activeTabId = id; + this.wirePageEvents(page); + // Inject indicator on the new tab + page.evaluate(indicatorScript).catch(() => {}); + console.log(`[browse] New tab detected (id=${id}, total=${this.pages.size})`); + }); + // Persistent context opens a default page — adopt it instead of creating a new one const existingPages = this.context.pages(); if (existingPages.length > 0) { @@ -414,6 +425,55 @@ export class BrowserManager { if (!this.pages.has(id)) throw new Error(`Tab ${id} not found`); this.activeTabId = id; this.activeFrame = null; // Frame context is per-tab + // Bring the page to front so the user sees the switch in the browser + const page = this.pages.get(id); + if (page) page.bringToFront().catch(() => {}); + } + + /** + * Sync activeTabId to match the tab whose URL matches the Chrome extension's + * active tab. Called on every /sidebar-tabs poll so manual tab switches in + * the browser are detected within ~2s. + */ + syncActiveTabByUrl(activeUrl: string): void { + if (!activeUrl || this.pages.size <= 1) return; + // Try exact match first, then fuzzy match (origin+pathname, ignoring query/fragment) + let fuzzyId: number | null = null; + let activeOriginPath = ''; + try { + const u = new URL(activeUrl); + activeOriginPath = u.origin + u.pathname; + } catch {} + + for (const [id, page] of this.pages) { + try { + const pageUrl = page.url(); + // Exact match — best case + if (pageUrl === activeUrl && id !== this.activeTabId) { + this.activeTabId = id; + this.activeFrame = null; + return; + } + // Fuzzy match — origin+pathname (handles query param / fragment differences) + if (activeOriginPath && fuzzyId === null && id !== this.activeTabId) { + try { + const pu = new URL(pageUrl); + if (pu.origin + pu.pathname === activeOriginPath) { + fuzzyId = id; + } + } catch {} + } + } catch {} + } + // Fall back to fuzzy match + if (fuzzyId !== null) { + this.activeTabId = fuzzyId; + this.activeFrame = null; + } + } + + getActiveTabId(): number { + return this.activeTabId; } getTabCount(): number { @@ -876,6 +936,22 @@ export class BrowserManager { // ─── Console/Network/Dialog/Ref Wiring ──────────────────── private wirePageEvents(page: Page) { + // Track tab close — remove from pages map, switch to another tab + page.on('close', () => { + for (const [id, p] of this.pages) { + if (p === page) { + this.pages.delete(id); + console.log(`[browse] Tab closed (id=${id}, remaining=${this.pages.size})`); + // If the closed tab was active, switch to another + if (this.activeTabId === id) { + const remaining = [...this.pages.keys()]; + this.activeTabId = remaining.length > 0 ? remaining[remaining.length - 1] : 0; + } + break; + } + } + }); + // Clear ref map on navigation — refs point to stale elements after page change // (lastSnapshot is NOT cleared — it's a text baseline for diffing) page.on('framenavigated', (frame) => {