Skip to content
Merged
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
4 changes: 3 additions & 1 deletion packages/bcode-browser/skills/cloud-browser.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,14 @@ The response carries more than the three fields above. Other fields you may want
- `recordingUrl` — playback URL for the session recording. Surface this to the user when handing back the run.
- `status`, `startedAt`, `finishedAt`, `proxyUsedMb`, `proxyCost`, `browserCost`, `agentSessionId` — observability fields, not needed to drive the browser.

The `liveUrl` is a viewer URL the user can open in their own browser to watch the cloud browser's pixels. **Print it to console** so the user can click it:
The `liveUrl` is a viewer URL the user can open in their own browser to watch the cloud browser's pixels. **Print it to console** so the user can see it:

```js
console.log("Cloud browser ready. Live view:", liveUrl)
```

If the user later asks for the link in a clickable form (e.g. "give me the live url"), surface it in your reply as a markdown link — `[Live view](<liveUrl>)` — which the TUI renders clickable. Tool stdout is not auto-linkified, but markdown in your assistant message is.

Stash `id` somewhere (a `globalThis.cloudBrowserId = id` is fine, or the snippet's return value) — you need it to stop the browser later.

## Connect
Expand Down
4 changes: 4 additions & 0 deletions packages/bcode-browser/src/browser-execute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ const DEFAULT_TIMEOUT_MS = 60 * 1000
const MAX_TIMEOUT_MS = 10 * 60 * 1000

export const parameters = Schema.Struct({
description: Schema.String.annotate({
description:
"Clear, concise summary of what this snippet does in 3-7 words. Examples:\nInput: code that connects to local Chrome\nOutput: Connect to local Chrome\n\nInput: scrape product titles from current page\nOutput: Scrape product titles\n\nInput: capture a screenshot of the homepage\nOutput: Screenshot homepage",
}),
code: Schema.String.annotate({
description:
"JavaScript source. Wrapped in an async function with `session` (CDP Session) and `console` (per-call capture; same `log/error/warn/info` API) bound.",
Expand Down
8 changes: 8 additions & 0 deletions packages/bcode-browser/test/browser-execute.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ test.skipIf(!enabled)("connect + console.log + return value", async () => {
const impl = yield* BrowserExecute.make(dataDir)
return yield* impl.execute(
{
description: "Connect to local Chrome",
code: `await session.connect({ profileDir: ${JSON.stringify(profileDir!)}, timeoutMs: 5000 });
console.log("connected", session.isConnected());
return { ok: session.isConnected() };`,
Expand All @@ -58,6 +59,7 @@ test.skipIf(!enabled)("Session is reused across calls (SessionStore)", async ()
const impl = yield* BrowserExecute.make(dataDir)
return yield* impl.execute(
{
description: "Verify session reuse",
code: `// connect was called in the previous test on the same sessionID.
console.log("still connected:", session.isConnected());
return session.isConnected();`,
Expand Down Expand Up @@ -99,6 +101,7 @@ test.skipIf(!enabled)("workspace import inside a snippet", async () => {
const impl = yield* BrowserExecute.make(dataDir)
return yield* impl.execute(
{
description: "Import workspace module",
code: `const m = await import(${JSON.stringify(file)} + "?t=" + Date.now());
const t = await m.run(session);
console.log("title:", t);
Expand All @@ -120,6 +123,7 @@ test.skipIf(!enabled)("Page.captureScreenshot is collected into result.screensho
const impl = yield* BrowserExecute.make(dataDir)
return yield* impl.execute(
{
description: "Capture two screenshots",
code: `await session.Page.enable();
await session.Page.navigate({ url: "data:text/html,<title>shot</title><body>hi" });
await session.waitFor("Page.loadEventFired", undefined, 5000);
Expand Down Expand Up @@ -151,6 +155,7 @@ test.skipIf(!enabled)("BCODE_SCREENSHOT_DIR dumps screenshots to disk", async ()
const impl = yield* BrowserExecute.make(dataDir)
return yield* impl.execute(
{
description: "Dump screenshot to disk",
code: `await session.Page.captureScreenshot({ format: "png" });`,
},
{ sessionID, workspaceDir },
Expand Down Expand Up @@ -178,6 +183,7 @@ test.skipIf(!enabled)("syntax error in snippet surfaces a clean failure", async
const impl = yield* BrowserExecute.make(dataDir)
return yield* impl.execute(
{
description: "Trigger syntax error",
code: `const x = (`,
},
{ sessionID, workspaceDir },
Expand All @@ -200,6 +206,7 @@ test("console.debug is captured; uncommon methods fall through without throwing"
const impl = yield* BrowserExecute.make(data)
return yield* impl.execute(
{
description: "Exercise console methods",
code: `console.debug("captured-debug");
console.table([{a: 1}]);
console.trace("trace-call");
Expand Down Expand Up @@ -234,6 +241,7 @@ test("overlapping execute calls do not clobber each other's console capture", as
const impl = yield* BrowserExecute.make(dataDirX)
return yield* impl.execute(
{
description: `Concurrent snippet ${label}`,
// Yield once so both snippets' bodies are mid-execution at the same
// time; under the old global-patch impl, B's tee would shadow A's
// and the `finally` chain would corrupt both captures + the global.
Expand Down
2 changes: 1 addition & 1 deletion packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1883,7 +1883,7 @@ function BrowserExecute(props: ToolProps<typeof BrowserExecuteTool>) {
<Switch>
<Match when={props.metadata.output !== undefined}>
<BlockTool
title="# Browser execute"
title={`# ${props.input.description ?? "Browser execute"}`}
part={props.part}
spinner={isRunning()}
onClick={overflow() ? () => setExpanded((prev) => !prev) : undefined}
Expand Down
2 changes: 2 additions & 0 deletions packages/opencode/src/tool/browser-execute.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ Use this tool whenever the task requires driving a real browser — automation,

Before the first `browser_execute` call of a session, you MUST read `{{SKILLS_DIR}}/BROWSER.md`. It defines the snippet model, the three connection methods (local user Chrome, isolated debug-port Chrome, Browser Use cloud browser), the workspace pattern, the `session` API surface, and gotchas. For cloud-browser specifics, also read `{{SKILLS_DIR}}/cloud-browser.md`.

Always pass a clear, concise `description` of what the snippet does in 3-7 words (e.g. "Connect to local Chrome", "Scrape product titles", "Screenshot homepage"). It surfaces in the TUI as the call's title.

Snippet scope:

- `session` — the live CDP `Session`. You call `session.connect(...)` once at the start of your work; subsequent snippets reuse the same connection. Domain methods follow `session.<Domain>.<method>(params)` and return Promises.
Expand Down
Loading