Skip to content

Commit 3985e81

Browse files
author
alex
committed
feat(browser_execute): require description field; surface as TUI title
browser_execute now takes a required 'description' parameter that mirrors the bash tool's same-named field. The TUI's BlockTool title shows it instead of the static 'Browser execute' label, giving the user a per-call summary of what the snippet does. Strong models were already writing this as a comment at the top of 'code'; an explicit slot is cheaper and renders better. Also adds a one-line note to cloud-browser.md telling the agent to surface 'liveUrl' as a markdown link if the user asks for it (markdown links in assistant replies are clickable in the TUI; tool stdout is not).
1 parent ff90631 commit 3985e81

5 files changed

Lines changed: 18 additions & 2 deletions

File tree

packages/bcode-browser/skills/cloud-browser.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,14 @@ The response carries more than the three fields above. Other fields you may want
4141
- `recordingUrl` — playback URL for the session recording. Surface this to the user when handing back the run.
4242
- `status`, `startedAt`, `finishedAt`, `proxyUsedMb`, `proxyCost`, `browserCost`, `agentSessionId` — observability fields, not needed to drive the browser.
4343

44-
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:
44+
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:
4545

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

50+
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.
51+
5052
Stash `id` somewhere (a `globalThis.cloudBrowserId = id` is fine, or the snippet's return value) — you need it to stop the browser later.
5153

5254
## Connect

packages/bcode-browser/src/browser-execute.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ const DEFAULT_TIMEOUT_MS = 60 * 1000
5050
const MAX_TIMEOUT_MS = 10 * 60 * 1000
5151

5252
export const parameters = Schema.Struct({
53+
description: Schema.String.annotate({
54+
description:
55+
"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",
56+
}),
5357
code: Schema.String.annotate({
5458
description:
5559
"JavaScript source. Wrapped in an async function with `session` (CDP Session) and `console` (per-call capture; same `log/error/warn/info` API) bound.",

packages/bcode-browser/test/browser-execute.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ test.skipIf(!enabled)("connect + console.log + return value", async () => {
3838
const impl = yield* BrowserExecute.make(dataDir)
3939
return yield* impl.execute(
4040
{
41+
description: "Connect to local Chrome",
4142
code: `await session.connect({ profileDir: ${JSON.stringify(profileDir!)}, timeoutMs: 5000 });
4243
console.log("connected", session.isConnected());
4344
return { ok: session.isConnected() };`,
@@ -58,6 +59,7 @@ test.skipIf(!enabled)("Session is reused across calls (SessionStore)", async ()
5859
const impl = yield* BrowserExecute.make(dataDir)
5960
return yield* impl.execute(
6061
{
62+
description: "Verify session reuse",
6163
code: `// connect was called in the previous test on the same sessionID.
6264
console.log("still connected:", session.isConnected());
6365
return session.isConnected();`,
@@ -99,6 +101,7 @@ test.skipIf(!enabled)("workspace import inside a snippet", async () => {
99101
const impl = yield* BrowserExecute.make(dataDir)
100102
return yield* impl.execute(
101103
{
104+
description: "Import workspace module",
102105
code: `const m = await import(${JSON.stringify(file)} + "?t=" + Date.now());
103106
const t = await m.run(session);
104107
console.log("title:", t);
@@ -120,6 +123,7 @@ test.skipIf(!enabled)("Page.captureScreenshot is collected into result.screensho
120123
const impl = yield* BrowserExecute.make(dataDir)
121124
return yield* impl.execute(
122125
{
126+
description: "Capture two screenshots",
123127
code: `await session.Page.enable();
124128
await session.Page.navigate({ url: "data:text/html,<title>shot</title><body>hi" });
125129
await session.waitFor("Page.loadEventFired", undefined, 5000);
@@ -151,6 +155,7 @@ test.skipIf(!enabled)("BCODE_SCREENSHOT_DIR dumps screenshots to disk", async ()
151155
const impl = yield* BrowserExecute.make(dataDir)
152156
return yield* impl.execute(
153157
{
158+
description: "Dump screenshot to disk",
154159
code: `await session.Page.captureScreenshot({ format: "png" });`,
155160
},
156161
{ sessionID, workspaceDir },
@@ -178,6 +183,7 @@ test.skipIf(!enabled)("syntax error in snippet surfaces a clean failure", async
178183
const impl = yield* BrowserExecute.make(dataDir)
179184
return yield* impl.execute(
180185
{
186+
description: "Trigger syntax error",
181187
code: `const x = (`,
182188
},
183189
{ sessionID, workspaceDir },
@@ -200,6 +206,7 @@ test("console.debug is captured; uncommon methods fall through without throwing"
200206
const impl = yield* BrowserExecute.make(data)
201207
return yield* impl.execute(
202208
{
209+
description: "Exercise console methods",
203210
code: `console.debug("captured-debug");
204211
console.table([{a: 1}]);
205212
console.trace("trace-call");
@@ -234,6 +241,7 @@ test("overlapping execute calls do not clobber each other's console capture", as
234241
const impl = yield* BrowserExecute.make(dataDirX)
235242
return yield* impl.execute(
236243
{
244+
description: `Concurrent snippet ${label}`,
237245
// Yield once so both snippets' bodies are mid-execution at the same
238246
// time; under the old global-patch impl, B's tee would shadow A's
239247
// and the `finally` chain would corrupt both captures + the global.

packages/opencode/src/cli/cmd/tui/routes/session/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1883,7 +1883,7 @@ function BrowserExecute(props: ToolProps<typeof BrowserExecuteTool>) {
18831883
<Switch>
18841884
<Match when={props.metadata.output !== undefined}>
18851885
<BlockTool
1886-
title="# Browser execute"
1886+
title={`# ${props.input.description ?? "Browser execute"}`}
18871887
part={props.part}
18881888
spinner={isRunning()}
18891889
onClick={overflow() ? () => setExpanded((prev) => !prev) : undefined}

packages/opencode/src/tool/browser-execute.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ Use this tool whenever the task requires driving a real browser — automation,
44

55
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`.
66

7+
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.
8+
79
Snippet scope:
810

911
- `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.

0 commit comments

Comments
 (0)