diff --git a/packages/bcode-browser/skills/cloud-browser.md b/packages/bcode-browser/skills/cloud-browser.md index f7219c901..ea25ac390 100644 --- a/packages/bcode-browser/skills/cloud-browser.md +++ b/packages/bcode-browser/skills/cloud-browser.md @@ -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]()` — 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 diff --git a/packages/bcode-browser/src/browser-execute.ts b/packages/bcode-browser/src/browser-execute.ts index 64a6802d4..7f25447db 100644 --- a/packages/bcode-browser/src/browser-execute.ts +++ b/packages/bcode-browser/src/browser-execute.ts @@ -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.", diff --git a/packages/bcode-browser/test/browser-execute.test.ts b/packages/bcode-browser/test/browser-execute.test.ts index 20a4e5670..fa11b6c6e 100644 --- a/packages/bcode-browser/test/browser-execute.test.ts +++ b/packages/bcode-browser/test/browser-execute.test.ts @@ -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() };`, @@ -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();`, @@ -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); @@ -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,shothi" }); await session.waitFor("Page.loadEventFired", undefined, 5000); @@ -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 }, @@ -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 }, @@ -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"); @@ -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. diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index e375c3799..74f690998 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -1883,7 +1883,7 @@ function BrowserExecute(props: ToolProps) { setExpanded((prev) => !prev) : undefined} diff --git a/packages/opencode/src/tool/browser-execute.txt b/packages/opencode/src/tool/browser-execute.txt index 62a73ec53..83d9b148c 100644 --- a/packages/opencode/src/tool/browser-execute.txt +++ b/packages/opencode/src/tool/browser-execute.txt @@ -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..(params)` and return Promises.