mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-02 11:45:20 +02:00
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user