From c3785e09cca32a1d7eb0d188c143e8fc38b2d0d8 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Mon, 6 Apr 2026 16:51:49 -0700 Subject: [PATCH] refactor: update handler signatures to use TabSession Change handleReadCommand and handleSnapshot to take TabSession instead of BrowserManager. Change handleWriteCommand to take both TabSession (per-tab ops) and BrowserManager (global ops like viewport, headers, dialog). handleMetaCommand keeps BrowserManager for tab management. Tests use thin wrapper functions that bridge the old 3-arg call pattern to the new signatures via bm.getActiveSession(). Co-Authored-By: Claude Opus 4.6 (1M context) --- browse/src/cookie-picker-routes.ts | 4 +-- browse/src/meta-commands.ts | 41 ++++++++++++++++-------------- browse/src/read-commands.ts | 16 ++++++------ browse/src/server.ts | 8 +++--- browse/src/snapshot.ts | 24 ++++++++--------- browse/src/write-commands.ts | 26 ++++++++++--------- browse/test/commands.test.ts | 10 ++++++-- browse/test/compare-board.test.ts | 9 +++++-- browse/test/handoff.test.ts | 5 +++- browse/test/snapshot.test.ts | 9 +++++-- 10 files changed, 89 insertions(+), 63 deletions(-) diff --git a/browse/src/cookie-picker-routes.ts b/browse/src/cookie-picker-routes.ts index 6b8499a0..775fc0d0 100644 --- a/browse/src/cookie-picker-routes.ts +++ b/browse/src/cookie-picker-routes.ts @@ -155,7 +155,7 @@ export async function handleCookiePickerRoute( } // Add to Playwright context - const page = bm.getPage(); + const page = bm.getActiveSession().getPage(); await page.context().addCookies(result.cookies); // Track what was imported @@ -187,7 +187,7 @@ export async function handleCookiePickerRoute( return errorResponse("Missing or empty 'domains' array", 'missing_param', { port }); } - const page = bm.getPage(); + const page = bm.getActiveSession().getPage(); const context = page.context(); for (const domain of domains) { await context.clearCookies({ domain }); diff --git a/browse/src/meta-commands.ts b/browse/src/meta-commands.ts index e2060c21..aca948e5 100644 --- a/browse/src/meta-commands.ts +++ b/browse/src/meta-commands.ts @@ -50,6 +50,9 @@ export async function handleMetaCommand( bm: BrowserManager, shutdown: () => Promise | void ): Promise { + // Per-tab operations use the active session; global operations use bm directly + const session = bm.getActiveSession(); + switch (command) { // ─── Tabs ────────────────────────────────────────── case 'tabs': { @@ -80,7 +83,7 @@ export async function handleMetaCommand( // ─── Server Control ──────────────────────────────── case 'status': { - const page = bm.getPage(); + const page = session.getPage(); const tabs = bm.getTabCount(); const mode = bm.getConnectionMode(); return [ @@ -111,7 +114,7 @@ export async function handleMetaCommand( // ─── Visual ──────────────────────────────────────── case 'screenshot': { // Parse priority: flags (--viewport, --clip) → selector (@ref, CSS) → output path - const page = bm.getPage(); + const page = session.getPage(); let outputPath = `${TEMP_DIR}/browse-screenshot.png`; let clipRect: { x: number; y: number; width: number; height: number } | undefined; let targetSelector: string | undefined; @@ -158,7 +161,7 @@ export async function handleMetaCommand( } if (targetSelector) { - const resolved = await bm.resolveRef(targetSelector); + const resolved = await session.resolveRef(targetSelector); const locator = 'locator' in resolved ? resolved.locator : page.locator(resolved.selector); await locator.screenshot({ path: outputPath, timeout: 5000 }); return `Screenshot saved (element): ${outputPath}`; @@ -174,7 +177,7 @@ export async function handleMetaCommand( } case 'pdf': { - const page = bm.getPage(); + const page = session.getPage(); const pdfPath = args[0] || `${TEMP_DIR}/browse-page.pdf`; validateOutputPath(pdfPath); await page.pdf({ path: pdfPath, format: 'A4' }); @@ -182,7 +185,7 @@ export async function handleMetaCommand( } case 'responsive': { - const page = bm.getPage(); + const page = session.getPage(); const prefix = args[0] || `${TEMP_DIR}/browse-responsive`; validateOutputPath(prefix); const viewports = [ @@ -238,10 +241,10 @@ export async function handleMetaCommand( try { let result: string; if (WRITE_COMMANDS.has(name)) { - result = await handleWriteCommand(name, cmdArgs, bm); + result = await handleWriteCommand(name, cmdArgs, session, bm); lastWasWrite = true; } else if (READ_COMMANDS.has(name)) { - result = await handleReadCommand(name, cmdArgs, bm); + result = await handleReadCommand(name, cmdArgs, session); if (PAGE_CONTENT_COMMANDS.has(name)) { result = wrapUntrustedContent(result, bm.getCurrentUrl()); } @@ -260,7 +263,7 @@ export async function handleMetaCommand( // Wait for network to settle after write commands before returning if (lastWasWrite) { - await bm.getPage().waitForLoadState('networkidle', { timeout: 2000 }).catch(() => {}); + await session.getPage().waitForLoadState('networkidle', { timeout: 2000 }).catch(() => {}); } return results.join('\n\n'); @@ -271,7 +274,7 @@ export async function handleMetaCommand( const [url1, url2] = args; if (!url1 || !url2) throw new Error('Usage: browse diff '); - const page = bm.getPage(); + const page = session.getPage(); await validateNavigationUrl(url1); await page.goto(url1, { waitUntil: 'domcontentloaded', timeout: 15000 }); const text1 = await getCleanText(page); @@ -296,7 +299,7 @@ export async function handleMetaCommand( // ─── Snapshot ───────────────────────────────────── case 'snapshot': { - const snapshotResult = await handleSnapshot(args, bm); + const snapshotResult = await handleSnapshot(args, session); return wrapUntrustedContent(snapshotResult, bm.getCurrentUrl()); } @@ -309,7 +312,7 @@ export async function handleMetaCommand( case 'resume': { bm.resume(); // Re-snapshot to capture current page state after human interaction - const snapshot = await handleSnapshot(['-i'], bm); + const snapshot = await handleSnapshot(['-i'], session); return `RESUMED\n${wrapUntrustedContent(snapshot, bm.getCurrentUrl())}`; } @@ -359,7 +362,7 @@ export async function handleMetaCommand( // If a ref was passed, scroll it into view if (args.length > 0 && args[0].startsWith('@')) { try { - const resolved = await bm.resolveRef(args[0]); + const resolved = await session.resolveRef(args[0]); if ('locator' in resolved) { await resolved.locator.scrollIntoViewIfNeeded({ timeout: 5000 }); return `Browser activated. Scrolled ${args[0]} into view.`; @@ -504,7 +507,7 @@ export async function handleMetaCommand( } } // Close existing pages, then restore (replace, not merge) - bm.setFrame(null); + session.setFrame(null); await bm.closeAllPages(); await bm.restoreState({ cookies: data.cookies, @@ -522,12 +525,12 @@ export async function handleMetaCommand( if (!target) throw new Error('Usage: frame '); if (target === 'main') { - bm.setFrame(null); - bm.clearRefs(); + session.setFrame(null); + session.clearRefs(); return 'Switched to main frame'; } - const page = bm.getPage(); + const page = session.getPage(); let frame: Frame | null = null; if (target === '--name') { @@ -538,7 +541,7 @@ export async function handleMetaCommand( frame = page.frame({ url: new RegExp(args[1]) }); } else { // CSS selector or @ref for the iframe element - const resolved = await bm.resolveRef(target); + const resolved = await session.resolveRef(target); const locator = 'locator' in resolved ? resolved.locator : page.locator(resolved.selector); const elementHandle = await locator.elementHandle({ timeout: 5000 }); frame = await elementHandle?.contentFrame() ?? null; @@ -546,8 +549,8 @@ export async function handleMetaCommand( } if (!frame) throw new Error(`Frame not found: ${target}`); - bm.setFrame(frame); - bm.clearRefs(); + session.setFrame(frame); + session.clearRefs(); return `Switched to frame: ${frame.url()}`; } diff --git a/browse/src/read-commands.ts b/browse/src/read-commands.ts index 83c791a3..ace2f552 100644 --- a/browse/src/read-commands.ts +++ b/browse/src/read-commands.ts @@ -5,7 +5,7 @@ * console, network, cookies, storage, perf */ -import type { BrowserManager } from './browser-manager'; +import type { TabSession } from './tab-session'; import { consoleBuffer, networkBuffer, dialogBuffer } from './buffers'; import type { Page, Frame } from 'playwright'; import * as fs from 'fs'; @@ -90,11 +90,11 @@ export async function getCleanText(page: Page | Frame): Promise { export async function handleReadCommand( command: string, args: string[], - bm: BrowserManager + session: TabSession ): Promise { - const page = bm.getPage(); + const page = session.getPage(); // Frame-aware target for content extraction - const target = bm.getActiveFrameOrPage(); + const target = session.getActiveFrameOrPage(); switch (command) { case 'text': { @@ -104,7 +104,7 @@ export async function handleReadCommand( case 'html': { const selector = args[0]; if (selector) { - const resolved = await bm.resolveRef(selector); + const resolved = await session.resolveRef(selector); if ('locator' in resolved) { return await resolved.locator.innerHTML({ timeout: 5000 }); } @@ -186,7 +186,7 @@ export async function handleReadCommand( case 'css': { const [selector, property] = args; if (!selector || !property) throw new Error('Usage: browse css '); - const resolved = await bm.resolveRef(selector); + const resolved = await session.resolveRef(selector); if ('locator' in resolved) { const value = await resolved.locator.evaluate( (el, prop) => getComputedStyle(el).getPropertyValue(prop), @@ -208,7 +208,7 @@ export async function handleReadCommand( case 'attrs': { const selector = args[0]; if (!selector) throw new Error('Usage: browse attrs '); - const resolved = await bm.resolveRef(selector); + const resolved = await session.resolveRef(selector); if ('locator' in resolved) { const attrs = await resolved.locator.evaluate((el) => { const result: Record = {}; @@ -272,7 +272,7 @@ export async function handleReadCommand( const selector = args[1]; if (!property || !selector) throw new Error('Usage: browse is \nProperties: visible, hidden, enabled, disabled, checked, editable, focused'); - const resolved = await bm.resolveRef(selector); + const resolved = await session.resolveRef(selector); let locator; if ('locator' in resolved) { locator = resolved.locator; diff --git a/browse/src/server.ts b/browse/src/server.ts index 2488a4f1..b7826ee7 100644 --- a/browse/src/server.ts +++ b/browse/src/server.ts @@ -823,13 +823,15 @@ async function handleCommand(body: any): Promise { try { let result: string; + const session = browserManager.getActiveSession(); + if (READ_COMMANDS.has(command)) { - result = await handleReadCommand(command, args, browserManager); + result = await handleReadCommand(command, args, session); if (PAGE_CONTENT_COMMANDS.has(command)) { result = wrapUntrustedContent(result, browserManager.getCurrentUrl()); } } else if (WRITE_COMMANDS.has(command)) { - result = await handleWriteCommand(command, args, browserManager); + result = await handleWriteCommand(command, args, session, browserManager); } else if (META_COMMANDS.has(command)) { result = await handleMetaCommand(command, args, browserManager, shutdown); // Start periodic snapshot interval when watch mode begins @@ -840,7 +842,7 @@ async function handleCommand(body: any): Promise { return; } try { - const snapshot = await handleSnapshot(['-i'], browserManager); + const snapshot = await handleSnapshot(['-i'], browserManager.getActiveSession()); browserManager.addWatchSnapshot(snapshot); } catch { // Page may be navigating — skip this snapshot diff --git a/browse/src/snapshot.ts b/browse/src/snapshot.ts index ae18c3f3..616f5c37 100644 --- a/browse/src/snapshot.ts +++ b/browse/src/snapshot.ts @@ -18,7 +18,7 @@ */ import type { Page, Frame, Locator } from 'playwright'; -import type { BrowserManager, RefEntry } from './browser-manager'; +import type { TabSession, RefEntry } from './tab-session'; import * as Diff from 'diff'; import { TEMP_DIR, isPathWithin } from './platform'; @@ -132,13 +132,13 @@ function parseLine(line: string): ParsedNode | null { */ export async function handleSnapshot( args: string[], - bm: BrowserManager + session: TabSession ): Promise { const opts = parseSnapshotArgs(args); - const page = bm.getPage(); + const page = session.getPage(); // Frame-aware target for accessibility tree - const target = bm.getActiveFrameOrPage(); - const inFrame = bm.getFrame() !== null; + const target = session.getActiveFrameOrPage(); + const inFrame = session.getFrame() !== null; // Get accessibility tree via ariaSnapshot let rootLocator: Locator; @@ -152,7 +152,7 @@ export async function handleSnapshot( const ariaText = await rootLocator.ariaSnapshot(); if (!ariaText || ariaText.trim().length === 0) { - bm.setRefMap(new Map()); + session.setRefMap(new Map()); return '(no accessible elements found)'; } @@ -337,7 +337,7 @@ export async function handleSnapshot( } // Store ref map on BrowserManager - bm.setRefMap(refMap); + session.setRefMap(refMap); if (output.length === 0) { return '(no interactive elements found)'; @@ -408,9 +408,9 @@ export async function handleSnapshot( // ─── Diff mode (-D) ─────────────────────────────────────── if (opts.diff) { - const lastSnapshot = bm.getLastSnapshot(); + const lastSnapshot = session.getLastSnapshot(); if (!lastSnapshot) { - bm.setLastSnapshot(snapshotText); + session.setLastSnapshot(snapshotText); return snapshotText + '\n\n(no previous snapshot to diff against — this snapshot stored as baseline)'; } @@ -425,16 +425,16 @@ export async function handleSnapshot( } } - bm.setLastSnapshot(snapshotText); + session.setLastSnapshot(snapshotText); return diffOutput.join('\n'); } // Store for future diffs - bm.setLastSnapshot(snapshotText); + session.setLastSnapshot(snapshotText); // Add frame context header when operating inside an iframe if (inFrame) { - const frameUrl = bm.getFrame()?.url() ?? 'unknown'; + const frameUrl = session.getFrame()?.url() ?? 'unknown'; output.unshift(`[Context: iframe src="${frameUrl}"]`); } diff --git a/browse/src/write-commands.ts b/browse/src/write-commands.ts index 5314795e..d0eeb544 100644 --- a/browse/src/write-commands.ts +++ b/browse/src/write-commands.ts @@ -5,6 +5,7 @@ * press, scroll, wait, viewport, cookie, header, useragent */ +import type { TabSession } from './tab-session'; import type { BrowserManager } from './browser-manager'; import { findInstalledBrowsers, importCookies, listSupportedBrowserNames } from './cookie-import-browser'; import { validateNavigationUrl } from './url-validation'; @@ -165,12 +166,13 @@ const CLEANUP_SELECTORS = { export async function handleWriteCommand( command: string, args: string[], + session: TabSession, bm: BrowserManager ): Promise { - const page = bm.getPage(); + const page = session.getPage(); // Frame-aware target for locator-based operations (click, fill, etc.) - const target = bm.getActiveFrameOrPage(); - const inFrame = bm.getFrame() !== null; + const target = session.getActiveFrameOrPage(); + const inFrame = session.getFrame() !== null; switch (command) { case 'goto': { @@ -206,9 +208,9 @@ export async function handleWriteCommand( if (!selector) throw new Error('Usage: browse click '); // Auto-route: if ref points to a real