Skip to content

Commit cff30f9

Browse files
committed
Nick: fixes interact tabs
1 parent ae1cd9d commit cff30f9

3 files changed

Lines changed: 118 additions & 32 deletions

File tree

apps/api/src/__tests__/snips/v2/scrape-browser.test.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,63 @@ describe("Scrape browser interact replay", () => {
191191
scrapeTimeout,
192192
);
193193

194+
itIf(canRunReplayHappyPath)(
195+
"opens a single content tab (no stray blank tab) when a session starts",
196+
async () => {
197+
const url = `${TEST_SUITE_WEBSITE}?testId=${crypto.randomUUID()}`;
198+
let scrapeId: string | null = null;
199+
200+
try {
201+
const scrapeResponse = await scrapeRaw(
202+
{
203+
url,
204+
origin: "website-replay-test",
205+
},
206+
identity,
207+
);
208+
209+
expect(scrapeResponse.statusCode).toBe(200);
210+
expect(scrapeResponse.body.success).toBe(true);
211+
expect(typeof scrapeResponse.body.scrape_id).toBe("string");
212+
scrapeId = scrapeResponse.body.scrape_id as string;
213+
214+
// Session creation primes agent-browser and consolidates tabs, so
215+
// user code must see exactly one tab: the content page.
216+
const executeResponse = await interactWithReplicaRetry(
217+
scrapeId,
218+
{
219+
language: "node",
220+
timeout: 60,
221+
code: `
222+
console.log(JSON.stringify(page.context().pages().map(p => p.url())));
223+
`,
224+
},
225+
identity,
226+
);
227+
228+
expect(executeResponse.statusCode).toBe(200);
229+
expect(executeResponse.body.success).toBe(true);
230+
231+
const lastLine =
232+
executeResponse.body.stdout
233+
?.trim()
234+
.split("\n")
235+
.filter(Boolean)
236+
.pop() ?? "[]";
237+
const tabUrls = JSON.parse(lastLine) as string[];
238+
239+
expect(tabUrls).toHaveLength(1);
240+
expect(tabUrls[0]).not.toBe("about:blank");
241+
expect(tabUrls[0]).toContain(TEST_SUITE_WEBSITE);
242+
} finally {
243+
if (scrapeId) {
244+
await scrapeStopInteractiveBrowserRaw(scrapeId, identity);
245+
}
246+
}
247+
},
248+
scrapeTimeout,
249+
);
250+
194251
itIf(!TEST_SELF_HOST)(
195252
"returns 400 for invalid scrape job id format",
196253
async () => {

apps/api/src/controllers/v2/scrape-browser.ts

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -703,9 +703,21 @@ async function createSessionForScrape(
703703
);
704704
}
705705

706-
// Ensure only one tab exists with the content page in the foreground.
707-
// The replay may have created extra tabs. Find the one with content,
708-
// close everything else, update the REPL's page var, and bring to front.
706+
// Prime agent-browser before consolidating: its first command of a session
707+
// spawns an about:blank tab, so trigger it now and let the sync below close it.
708+
const primeResult = await browserServiceRequest<BrowserServiceExecResponse>(
709+
"POST",
710+
`/browsers/${svcResponse.sessionId}/exec`,
711+
{
712+
code: `agent-browser get url`,
713+
language: "bash",
714+
timeout: 10,
715+
origin: "scrape_replay_sync",
716+
},
717+
).catch(() => null);
718+
719+
// Keep only the content tab, repoint the REPL's page var, bring to front.
720+
// agent-browser falls back to the surviving tab when its own is closed.
709721
await browserServiceRequest(
710722
"POST",
711723
`/browsers/${svcResponse.sessionId}/exec`,
@@ -726,19 +738,23 @@ async function createSessionForScrape(
726738
},
727739
).catch(() => {});
728740

729-
// Sync agent-browser to the correct page
730-
const syncResult = await browserServiceRequest<BrowserServiceExecResponse>(
731-
"POST",
732-
`/browsers/${svcResponse.sessionId}/exec`,
733-
{
734-
code: `agent-browser get url`,
735-
language: "bash",
736-
timeout: 10,
737-
origin: "scrape_replay_sync",
738-
},
739-
);
741+
// Verify agent-browser is on the content page after cleanup.
742+
let agentUrl = (primeResult?.stdout || "").trim();
743+
if (!agentUrl || agentUrl === "about:blank") {
744+
const syncResult =
745+
await browserServiceRequest<BrowserServiceExecResponse>(
746+
"POST",
747+
`/browsers/${svcResponse.sessionId}/exec`,
748+
{
749+
code: `agent-browser get url`,
750+
language: "bash",
751+
timeout: 10,
752+
origin: "scrape_replay_sync",
753+
},
754+
);
755+
agentUrl = (syncResult.stdout || "").trim();
756+
}
740757

741-
const agentUrl = (syncResult.stdout || "").trim();
742758
if (!agentUrl || agentUrl === "about:blank") {
743759
logger.info("agent-browser on wrong page after replay, navigating", {
744760
agentUrl,

apps/api/src/lib/scrape-interact/browser-agent.ts

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,30 @@ async function takeSnapshot(browserId: string): Promise<string> {
181181
}
182182
}
183183

184+
/**
185+
* Keep a single foregrounded content tab, closing agent-browser's stray
186+
* about:blank tab (it safely falls back to the surviving tab).
187+
*/
188+
async function syncTabs(browserId: string): Promise<void> {
189+
try {
190+
await browserServiceRequest("POST", `/browsers/${browserId}/exec`, {
191+
code: [
192+
`const ctx = page.context();`,
193+
`const pages = ctx.pages();`,
194+
`if (pages.length > 1) {`,
195+
` const target = pages.find(p => { const u = p.url(); return u && u !== 'about:blank'; }) || pages[pages.length - 1];`,
196+
` for (const p of pages) { if (p !== target) await p.close().catch(() => {}); }`,
197+
` page = target;`,
198+
`}`,
199+
`await page.bringToFront();`,
200+
].join("\n"),
201+
language: "node",
202+
timeout: 5,
203+
origin: "tab_sync",
204+
});
205+
} catch {}
206+
}
207+
184208
// ---------------------------------------------------------------------------
185209
// Main agent — tool-calling loop via AI SDK
186210
// ---------------------------------------------------------------------------
@@ -213,6 +237,11 @@ export async function executePromptViaBrowserAgent(
213237
debugLog.add(`Prompt: ${prompt}\n`);
214238
logger.info("Agent debug log", { path: debugLog.getPath() });
215239

240+
// Prime agent-browser and consolidate tabs first: its first command of a
241+
// session spawns an about:blank tab that would otherwise get snapshotted.
242+
await getCurrentUrl(browserId);
243+
await syncTabs(browserId);
244+
216245
const [initialSnapshot, initialUrl] = await Promise.all([
217246
takeSnapshot(browserId),
218247
getCurrentUrl(browserId),
@@ -258,23 +287,7 @@ export async function executePromptViaBrowserAgent(
258287
const output = (result.stdout || result.result || "").trim();
259288

260289
// Ensure only one tab exists and it's in the foreground for live view
261-
try {
262-
await browserServiceRequest("POST", `/browsers/${browserId}/exec`, {
263-
code: [
264-
`const ctx = page.context();`,
265-
`const pages = ctx.pages();`,
266-
`if (pages.length > 1) {`,
267-
` const target = pages.find(p => { const u = p.url(); return u && u !== 'about:blank'; }) || pages[pages.length - 1];`,
268-
` for (const p of pages) { if (p !== target) await p.close().catch(() => {}); }`,
269-
` page = target;`,
270-
`}`,
271-
`await page.bringToFront();`,
272-
].join("\n"),
273-
language: "node",
274-
timeout: 5,
275-
origin: "tab_sync",
276-
});
277-
} catch {}
290+
await syncTabs(browserId);
278291

279292
const elapsed = Date.now() - start;
280293

0 commit comments

Comments
 (0)