diff --git a/browse/src/browser-manager.ts b/browse/src/browser-manager.ts index 401b6cb0..1ef58e36 100644 --- a/browse/src/browser-manager.ts +++ b/browse/src/browser-manager.ts @@ -561,6 +561,10 @@ export class BrowserManager { * Use this for operations that work on both Page and Frame (locator, evaluate, etc.). */ getActiveFrameOrPage(): import('playwright').Page | import('playwright').Frame { + // Auto-recover from detached frames (iframe removed/navigated) + if (this.activeFrame?.isDetached()) { + this.activeFrame = null; + } return this.activeFrame ?? this.getPage(); } diff --git a/browse/src/meta-commands.ts b/browse/src/meta-commands.ts index b6f89def..4388491a 100644 --- a/browse/src/meta-commands.ts +++ b/browse/src/meta-commands.ts @@ -219,7 +219,9 @@ export async function handleMetaCommand( if (!Array.isArray(commands)) throw new Error('not array'); } catch { // Fallback: pipe-delimited format "goto url | click @e5 | snapshot -ic" - commands = jsonStr.split(' | ').map(seg => tokenizePipeSegment(seg.trim())); + commands = jsonStr.split(' | ') + .filter(seg => seg.trim().length > 0) + .map(seg => tokenizePipeSegment(seg.trim())); } const results: string[] = []; @@ -478,7 +480,11 @@ export async function handleMetaCommand( if (action === 'load') { if (!fs.existsSync(statePath)) throw new Error(`State not found: ${statePath}`); const data = JSON.parse(fs.readFileSync(statePath, 'utf-8')); + if (!Array.isArray(data.cookies) || !Array.isArray(data.pages)) { + throw new Error('Invalid state file: expected cookies and pages arrays'); + } // Close existing pages, then restore (replace, not merge) + bm.setFrame(null); await bm.closeAllPages(); await bm.restoreState({ cookies: data.cookies, @@ -516,6 +522,7 @@ export async function handleMetaCommand( const locator = 'locator' in resolved ? resolved.locator : page.locator(resolved.selector); const elementHandle = await locator.elementHandle({ timeout: 5000 }); frame = await elementHandle?.contentFrame() ?? null; + await elementHandle?.dispose(); } if (!frame) throw new Error(`Frame not found: ${target}`); diff --git a/browse/src/snapshot.ts b/browse/src/snapshot.ts index ae07b6b8..840cd686 100644 --- a/browse/src/snapshot.ts +++ b/browse/src/snapshot.ts @@ -208,11 +208,11 @@ export async function handleSnapshot( let locator: Locator; if (opts.selector) { - locator = page.locator(opts.selector).getByRole(node.role as any, { + locator = target.locator(opts.selector).getByRole(node.role as any, { name: node.name || undefined, }); } else { - locator = page.getByRole(node.role as any, { + locator = target.getByRole(node.role as any, { name: node.name || undefined, }); } @@ -236,7 +236,7 @@ export async function handleSnapshot( // ─── Cursor-interactive scan (-C) ───────────────────────── if (opts.cursorInteractive) { try { - const cursorElements = await page.evaluate(() => { + const cursorElements = await target.evaluate(() => { const STANDARD_INTERACTIVE = new Set([ 'A', 'BUTTON', 'INPUT', 'SELECT', 'TEXTAREA', 'SUMMARY', 'DETAILS', ]); @@ -290,7 +290,7 @@ export async function handleSnapshot( let cRefCounter = 1; for (const elem of cursorElements) { const ref = `c${cRefCounter++}`; - const locator = page.locator(elem.selector); + const locator = target.locator(elem.selector); refMap.set(ref, { locator, role: 'cursor-interactive', name: elem.text }); output.push(`@${ref} [${elem.reason}] "${elem.text}"`); } diff --git a/browse/src/write-commands.ts b/browse/src/write-commands.ts index f8e113b9..02413daf 100644 --- a/browse/src/write-commands.ts +++ b/browse/src/write-commands.ts @@ -258,7 +258,7 @@ export async function handleWriteCommand( if ('locator' in resolved) { await resolved.locator.setInputFiles(filePaths); } else { - await page.locator(resolved.selector).setInputFiles(filePaths); + await target.locator(resolved.selector).setInputFiles(filePaths); } const fileInfo = filePaths.map(fp => {