From 59bed0f317affb2c1ec851070b6c5902fae3130b Mon Sep 17 00:00:00 2001 From: garrytan Date: Sun, 1 Nov 2026 00:18:59 +0000 Subject: [PATCH] fix: headed viewport auto keeps the user's active tab; render's closetab opts out of sessions Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --- browse/src/browser-manager.ts | 9 ++++++++- browse/test/session-name.test.ts | 5 ++++- browse/test/session-tabs.test.ts | 16 +++++++++++++--- lib/aside-render.ts | 5 +++-- 4 files changed, 28 insertions(+), 7 deletions(-) diff --git a/browse/src/browser-manager.ts b/browse/src/browser-manager.ts index e25b4605f..dfa27158e 100644 --- a/browse/src/browser-manager.ts +++ b/browse/src/browser-manager.ts @@ -1811,9 +1811,16 @@ export class BrowserManager { const pinned = this.getPage(); const loaded = this.getActiveSession().getLoadedHtml(); const url = pinned.url(); + const activeBefore = this.activeTabId; const page = await this.context.newPage(); + const listenerId = [...this.pages].find(([, p]) => p === page)?.[0]; + if (listenerId !== undefined) { + this.pages.delete(listenerId); + this.tabSessions.delete(listenerId); + } + this.activeTabId = activeBefore; const session = new TabSession(page); - this.wirePageEvents(page); + if (listenerId === undefined) this.wirePageEvents(page); try { if (loaded) await session.setTabContent(loaded.html, { waitUntil: loaded.waitUntil }); else if (url !== 'about:blank') await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 15000 }); diff --git a/browse/test/session-name.test.ts b/browse/test/session-name.test.ts index 440d2de88..8b2da4ddc 100644 --- a/browse/test/session-name.test.ts +++ b/browse/test/session-name.test.ts @@ -119,6 +119,9 @@ describe('CLI retry after a token change', () => { describe('gstack-render browse calls', () => { test('turn the automatic session off: render pins its own tab ids', () => { const source = fs.readFileSync(path.resolve(import.meta.dir, '../../lib/aside-render.ts'), 'utf8'); - expect(source).toMatch(/runProc\(bin, args, [^\n]*BROWSE_SESSION: ''/); + expect(source).toContain("const browseEnv = { ...process.env, BROWSE_SESSION: '' };"); + const calls = source.match(/runProc\(bin,[^\n]*/g) ?? []; + expect(calls.length).toBeGreaterThanOrEqual(2); + for (const call of calls) expect(call).toContain('browseEnv'); }); }); diff --git a/browse/test/session-tabs.test.ts b/browse/test/session-tabs.test.ts index 58074fd97..0273a4d23 100644 --- a/browse/test/session-tabs.test.ts +++ b/browse/test/session-tabs.test.ts @@ -225,11 +225,20 @@ const rootTab = bm.getActiveTabId(); const pinned = JSON.parse((await run('newtab', [U('vp'), '--json'])).result).tabId; bm.switchTab(userTab, { bringToFront: false }); const mode = bm.connectionMode; + const listener = (page) => { + const id = bm.nextTabId++; + bm.pages.set(id, page); + bm.tabSessions.set(id, new (bm.tabSessions.values().next().value.constructor)(page)); + bm.activeTabId = id; + }; + bm.context.on('page', listener); bm.connectionMode = 'headed'; let r; - try { r = await run('viewport', ['auto'], { tabId: pinned }); } finally { bm.connectionMode = mode; } + try { r = await run('viewport', ['auto'], { tabId: pinned }); } finally { bm.connectionMode = mode; bm.context.off('page', listener); } + const pinnedPage = bm.pages.get(pinned); steps.headedViewport = { status: r.status, userTracked: bm.pages.get(userTab) === userPage, - pinnedLive: bm.pages.has(pinned), pinnedUrl: bm.pages.get(pinned)?.url() ?? null }; + pinnedLive: bm.pages.has(pinned), pinnedUrl: pinnedPage?.url() ?? null, activeKept: bm.getActiveTabId() === userTab, + sameObject: [...bm.pages.values()].filter(p => p === pinnedPage).length, rootUrl: (await run('url')).result }; } { // state save and load inside a session land the session on its own saved page @@ -364,7 +373,8 @@ describe('session lifecycle', () => { expect(steps.headedNew).toEqual({ activeKept: true, added: 1, sameObject: 1 }); }); test('headed: viewport auto under a tab pin rebuilds only the pinned tab', () => { - expect(steps.headedViewport).toMatchObject({ status: 200, userTracked: true, pinnedLive: true }); + expect(steps.headedViewport).toMatchObject({ status: 200, userTracked: true, pinnedLive: true, activeKept: true, sameObject: 1 }); + expect(steps.headedViewport.rootUrl).not.toContain('s=vp'); expect(steps.headedViewport.pinnedUrl).toContain('s=vp'); }); test('state save and load inside a session leave it on its saved page', () => { diff --git a/lib/aside-render.ts b/lib/aside-render.ts index cf1b18601..af1edfa9f 100644 --- a/lib/aside-render.ts +++ b/lib/aside-render.ts @@ -526,8 +526,9 @@ export async function renderWithBrowse(spec: RenderSpec, bin: string | null = re if (target.kind === 'error') return fail(target.error); const deadline = Date.now() + (spec.timeoutMs ?? DEFAULT_TIMEOUT_MS); + const browseEnv = { ...process.env, BROWSE_SESSION: '' }; const run = async (args: string[]): Promise => { - const r = await runProc(bin, args, Math.max(1_000, Math.min(120_000, deadline - Date.now())), { ...process.env, BROWSE_SESSION: '' }); + const r = await runProc(bin, args, Math.max(1_000, Math.min(120_000, deadline - Date.now())), browseEnv); log.push(`$ browse ${args.join(' ').slice(0, 300)}\n${r.stdout}${r.stderr}`.trim()); if (r.error || r.code !== 0) { const first = (r.stderr || r.stdout || r.error || '').trim().split('\n')[0]; @@ -644,7 +645,7 @@ export async function renderWithBrowse(spec: RenderSpec, bin: string | null = re } catch (e) { return fail((e as Error).message); } finally { - if (tab !== undefined) await runProc(bin, ['closetab', String(tab)], 15_000); + if (tab !== undefined) await runProc(bin, ['closetab', String(tab)], 15_000, browseEnv); srv?.stop(); if (work) fs.rmSync(work, { recursive: true, force: true }); }