mirror of https://github.com/garrytan/gstack.git
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:
parent
da5d008197
commit
dcf1b0d7ef
|
|
@ -298,6 +298,17 @@ export class BrowserManager {
|
||||||
};
|
};
|
||||||
await this.context.addInitScript(indicatorScript);
|
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
|
// Persistent context opens a default page — adopt it instead of creating a new one
|
||||||
const existingPages = this.context.pages();
|
const existingPages = this.context.pages();
|
||||||
if (existingPages.length > 0) {
|
if (existingPages.length > 0) {
|
||||||
|
|
@ -414,6 +425,55 @@ export class BrowserManager {
|
||||||
if (!this.pages.has(id)) throw new Error(`Tab ${id} not found`);
|
if (!this.pages.has(id)) throw new Error(`Tab ${id} not found`);
|
||||||
this.activeTabId = id;
|
this.activeTabId = id;
|
||||||
this.activeFrame = null; // Frame context is per-tab
|
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 {
|
getTabCount(): number {
|
||||||
|
|
@ -876,6 +936,22 @@ export class BrowserManager {
|
||||||
|
|
||||||
// ─── Console/Network/Dialog/Ref Wiring ────────────────────
|
// ─── Console/Network/Dialog/Ref Wiring ────────────────────
|
||||||
private wirePageEvents(page: Page) {
|
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
|
// Clear ref map on navigation — refs point to stale elements after page change
|
||||||
// (lastSnapshot is NOT cleared — it's a text baseline for diffing)
|
// (lastSnapshot is NOT cleared — it's a text baseline for diffing)
|
||||||
page.on('framenavigated', (frame) => {
|
page.on('framenavigated', (frame) => {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue