Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/calm-pandas-wait.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"browse": patch
---

Keep CLI open requests alive while Stagehand initializes correctly provisioned local or Browserbase sessions.
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
},
"dependencies": {
"@browserbasehq/sdk": "^2.14.0",
"@browserbasehq/stagehand": "workspace:*",
"@oclif/core": "^4.11.0",
"@vercel/detect-agent": "^1.2.3",
"archiver": "^7.0.1",
Expand All @@ -58,7 +59,6 @@
"ignore": "^7.0.5",
"node-html-markdown": "^1.3.0",
"semver": "^7.7.4",
"stagehand-v3": "npm:@browserbasehq/stagehand@3.7.1",
"tsx": "^4.20.6",
"ws": "^8.18.3",
"zod": "^4.2.1"
Expand Down
27 changes: 7 additions & 20 deletions packages/cli/src/lib/driver/commands/elements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,8 @@ import type { DriverCommandHandlers } from "./types.js";
export const elementsHandlers: DriverCommandHandlers = {
async click(manager, params) {
const { selector } = z.object({ selector: z.string().min(1) }).parse(params);
const stagehand = await manager.stagehandInstance();
await stagehand.act({
arguments: [],
description: "click element",
method: "click",
selector: manager.resolveSelector(selector),
} as never);
const page = await manager.activePage();
await page.locator(manager.resolveSelector(selector)).click();
return { clicked: true };
},

Expand All @@ -23,15 +18,9 @@ export const elementsHandlers: DriverCommandHandlers = {
value: z.string(),
})
.parse(params);
const stagehand = await manager.stagehandInstance();
await stagehand.act({
arguments: [value],
description: "fill element",
method: "fill",
selector: manager.resolveSelector(selector),
} as never);
const page = await manager.activePage();
await page.locator(manager.resolveSelector(selector)).fill(value);
if (pressEnter) {
const page = await manager.activePage();
await page.keyPress("Enter");
}
return { filled: true, pressedEnter: pressEnter ?? false };
Expand All @@ -45,7 +34,7 @@ export const elementsHandlers: DriverCommandHandlers = {
})
.parse(params);
const page = await manager.activePage();
const selected = await page.deepLocator(manager.resolveSelector(selector)).selectOption(values);
const selected = await page.locator(manager.resolveSelector(selector)).selectOption(values);
return { selected };
},

Expand All @@ -57,9 +46,7 @@ export const elementsHandlers: DriverCommandHandlers = {
})
.parse(params);
const page = await manager.activePage();
await page
.deepLocator(manager.resolveSelector(selector))
.setInputFiles(files.length === 1 ? files[0]! : files);
await page.locator(manager.resolveSelector(selector)).setInputFiles(files);
return { files, uploaded: true };
},

Expand All @@ -72,7 +59,7 @@ export const elementsHandlers: DriverCommandHandlers = {
.parse(params);
const page = await manager.activePage();
await page
.deepLocator(manager.resolveSelector(selector))
.locator(manager.resolveSelector(selector))
.highlight({ durationMs: durationMs ?? 2000 });
return { highlighted: true };
},
Expand Down
42 changes: 23 additions & 19 deletions packages/cli/src/lib/driver/commands/mouse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ export const mouseHandlers: DriverCommandHandlers = {
y: z.number(),
})
.parse(params);
assertXPathUnavailable(returnXPath);
const page = await manager.activePage();
const xpath = await page.click(x, y, {
button,
clickCount,
returnXpath: returnXPath,
await page.click(x, y, {
...(button === undefined ? {} : { button }),
...(clickCount === undefined ? {} : { clickCount }),
});
return returnXPath ? { clicked: true, xpath } : { clicked: true };
return { clicked: true };
},

async "mouse.hover"(manager, params) {
Expand All @@ -32,9 +32,10 @@ export const mouseHandlers: DriverCommandHandlers = {
y: z.number(),
})
.parse(params);
assertXPathUnavailable(returnXPath);
const page = await manager.activePage();
const xpath = await page.hover(x, y, { returnXpath: returnXPath });
return returnXPath ? { hovered: true, xpath } : { hovered: true };
await page.hover(x, y);
return { hovered: true };
},

async "mouse.scroll"(manager, params) {
Expand All @@ -47,11 +48,10 @@ export const mouseHandlers: DriverCommandHandlers = {
y: z.number(),
})
.parse(params);
assertXPathUnavailable(returnXPath);
const page = await manager.activePage();
const xpath = await page.scroll(x, y, deltaX, deltaY, {
returnXpath: returnXPath,
});
return returnXPath ? { scrolled: true, xpath } : { scrolled: true };
await page.scroll(x, y, deltaX, deltaY);
return { scrolled: true };
},

async "mouse.drag"(manager, params) {
Expand All @@ -67,15 +67,19 @@ export const mouseHandlers: DriverCommandHandlers = {
toY: z.number(),
})
.parse(params);
assertXPathUnavailable(returnXPath);
const page = await manager.activePage();
const [fromXpath, toXpath] = await page.dragAndDrop(fromX, fromY, toX, toY, {
button,
delay,
returnXpath: returnXPath,
steps,
await page.dragAndDrop(fromX, fromY, toX, toY, {
...(button === undefined ? {} : { button }),
...(delay === undefined ? {} : { delay }),
...(steps === undefined ? {} : { steps }),
});
return returnXPath
? { dragged: true, fromXpath, toXpath, xpath: fromXpath }
: { dragged: true };
return { dragged: true };
},
};

function assertXPathUnavailable(returnXPath: boolean | undefined): void {
if (returnXPath) {
throw new Error("Coordinate XPath lookup is not exposed by Stagehand V4");
}
}
15 changes: 11 additions & 4 deletions packages/cli/src/lib/driver/commands/navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,28 +16,35 @@ export const navigationHandlers: DriverCommandHandlers = {
async open(manager, params) {
const { timeoutMs, url, waitUntil } = OpenSchema.parse(params);
const page = await manager.pageForOpen();
await page.goto(url, { timeoutMs, waitUntil });
await page.goto(url, pageNavigationOptions({ timeoutMs, waitUntil }));
return manager.openResult(page);
},

async reload(manager, params) {
const options = NavigationOptionsSchema.parse(params);
const page = await manager.activePage();
await page.reload(options);
await page.reload(pageNavigationOptions(options));
return manager.openResult(page);
},

async back(manager, params) {
const options = NavigationOptionsSchema.parse(params);
const page = await manager.activePage();
await page.goBack(options);
await page.goBack(pageNavigationOptions(options));
return manager.openResult(page);
},

async forward(manager, params) {
const options = NavigationOptionsSchema.parse(params);
const page = await manager.activePage();
await page.goForward(options);
await page.goForward(pageNavigationOptions(options));
return manager.openResult(page);
},
};

function pageNavigationOptions({ timeoutMs, waitUntil }: z.infer<typeof NavigationOptionsSchema>) {
return {
...(timeoutMs === undefined ? {} : { timeout: timeoutMs }),
...(waitUntil === undefined ? {} : { waitUntil }),
};
}
5 changes: 2 additions & 3 deletions packages/cli/src/lib/driver/commands/network.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import type { DriverCommandHandlers } from "./types.js";

export const networkHandlers: DriverCommandHandlers = {
async "network.on"(manager) {
const page = await manager.activePage();
return manager.network.enable(page);
async "network.on"() {
throw new Error("Network capture is not yet exposed by the Stagehand V4 client.");
},

async "network.off"(manager) {
Expand Down
6 changes: 3 additions & 3 deletions packages/cli/src/lib/driver/commands/page-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@ export const pageInfoHandlers: DriverCommandHandlers = {
.parse(params);
const page = await manager.activePage();

if (what === "url") return { url: page.url() };
if (what === "url") return { url: await page.url() };
if (what === "title") return { title: await page.title() };

const target = manager.resolveSelector(selector ?? "body");
const locator = page.deepLocator(target);
const locator = page.locator(target);

if (what === "text") return { text: await locator.textContent() };
if (what === "html") return { html: await locator.innerHtml() };
Expand All @@ -53,7 +53,7 @@ export const pageInfoHandlers: DriverCommandHandlers = {
})
.parse(params);
const page = await manager.activePage();
const locator = page.deepLocator(manager.resolveSelector(selector));
const locator = page.locator(manager.resolveSelector(selector));
return check === "visible"
? { visible: await locator.isVisible() }
: { checked: await locator.isChecked() };
Expand Down
8 changes: 3 additions & 5 deletions packages/cli/src/lib/driver/commands/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export const runtimeHandlers: DriverCommandHandlers = {
await fs.writeFile(options.path, buffer);
return { saved: options.path };
}
return { base64: buffer.toString("base64") };
return { base64: Buffer.from(buffer).toString("base64") };
},

async viewport(manager, params) {
Expand Down Expand Up @@ -85,10 +85,8 @@ export const runtimeHandlers: DriverCommandHandlers = {
return { waited: true };
},

async cursor(manager) {
const page = await manager.activePage();
await page.enableCursorOverlay();
return { cursor: "enabled" };
async cursor() {
throw new Error("The visible cursor overlay is not yet exposed by the Stagehand V4 client.");
},
};

Expand Down
45 changes: 23 additions & 22 deletions packages/cli/src/lib/driver/commands/tabs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,81 +12,82 @@ export const tabHandlers: DriverCommandHandlers = {
const { url } = z.object({ url: z.string().optional() }).parse(params);
const context = await manager.browserContext();
const page = await context.newPage(url);
context.setActivePage(page);
await context.setActivePage(page);
const pages = await context.pages();
return {
active: true,
index: context
.pages()
.findIndex((candidate: DriverPage) => candidate.targetId() === page.targetId()),
targetId: page.targetId(),
index: pages.findIndex((candidate: DriverPage) => candidate.pageId === page.pageId),
targetId: page.pageId,
title: await manager.safeTitle(page),
url: page.url(),
url: await page.url(),
};
},

async "tab.switch"(manager, params) {
const { tab } = z.object({ tab: z.string().min(1) }).parse(params);
const { index, page } = await resolveTab(manager, tab);
const context = await manager.browserContext();
context.setActivePage(page);
await context.setActivePage(page);
return {
index,
switched: true,
targetId: page.targetId(),
targetId: page.pageId,
title: await manager.safeTitle(page),
url: page.url(),
url: await page.url(),
};
},

async "tab.close"(manager, params) {
const { tab } = z.object({ tab: z.string().optional() }).parse(params);
const context = await manager.browserContext();
const pages = context.pages();
const pages = await context.pages();
if (pages.length === 1) {
throw new Error("Cannot close the last tab.");
}

const active = context.activePage();
const active = await context.activePage();
const resolved = tab ? await resolveTab(manager, tab) : resolveActiveTab(pages, active ?? null);
const closedTargetId = resolved.page.targetId();
const activeTargetId = active?.targetId();
const closedTargetId = resolved.page.pageId;
const activeTargetId = active?.pageId;
await resolved.page.close();
const remainingPages = context.pages().filter((page) => page.targetId() !== closedTargetId);
const remainingPages = (await context.pages()).filter((page) => page.pageId !== closedTargetId);
let selectedPage = activeTargetId
? remainingPages.find((page) => page.targetId() === activeTargetId)
? remainingPages.find((page) => page.pageId === activeTargetId)
: undefined;

if (!selectedPage) {
selectedPage =
remainingPages[Math.min(resolved.index, remainingPages.length - 1)] ?? remainingPages[0];
if (selectedPage) {
context.setActivePage(selectedPage);
await context.setActivePage(selectedPage);
}
}

return {
closed: true,
index: resolved.index,
selectedTargetId: selectedPage?.targetId(),
selectedTargetId: selectedPage?.pageId,
targetId: closedTargetId,
};
},
};

async function resolveTab(
manager: { browserContext: () => Promise<{ pages: () => DriverPage[] }> },
manager: {
browserContext: () => Promise<{ pages: () => Promise<DriverPage[]> }>;
},
tab: string,
): Promise<{ index: number; page: DriverPage }> {
const context = await manager.browserContext();
const pages = context.pages();
const pages = await context.pages();
const index = Number.parseInt(tab, 10);
if (/^\d+$/.test(tab)) {
const page = pages[index];
if (!page) throw new Error(`Tab index ${index} out of range (0-${pages.length - 1}).`);
return { index, page };
}

const targetIndex = pages.findIndex((page: DriverPage) => page.targetId() === tab);
const targetIndex = pages.findIndex((page: DriverPage) => page.pageId === tab);
if (targetIndex === -1) {
throw new Error(`Tab targetId ${tab} was not found. Run browse tab list for current tabs.`);
}
Expand All @@ -97,8 +98,8 @@ function resolveActiveTab(
pages: DriverPage[],
active: DriverPage | null,
): { index: number; page: DriverPage } {
const activeTargetId = active?.targetId();
const index = activeTargetId ? pages.findIndex((page) => page.targetId() === activeTargetId) : 0;
const activeTargetId = active?.pageId;
const index = activeTargetId ? pages.findIndex((page) => page.pageId === activeTargetId) : 0;
const page = pages[index] ?? pages[0];
if (!page) throw new Error("No active tab.");
return { index: index >= 0 ? index : 0, page };
Expand Down
Loading
Loading