From 1a00390b895db4911cb4e42429adfc60b38e3a1c Mon Sep 17 00:00:00 2001 From: bcode Date: Fri, 15 May 2026 21:52:09 -0700 Subject: [PATCH] feat(run): style browser_execute in headless run mode like bash Headless code run rendered browser_execute calls with the default gear icon and duplicated tool name (rowser_execute browser_execute). Mirror the existing TUI BrowserExecute component: REPL-style > icon, description as title, prompted code block (> first line, continuations), streamed output, and a completion line with timing. Mirrors the bash/shell handling in the same file. --- packages/opencode/src/cli/cmd/run/tool.ts | 68 +++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/packages/opencode/src/cli/cmd/run/tool.ts b/packages/opencode/src/cli/cmd/run/tool.ts index 3dab7aa8d..639ec848f 100644 --- a/packages/opencode/src/cli/cmd/run/tool.ts +++ b/packages/opencode/src/cli/cmd/run/tool.ts @@ -19,6 +19,7 @@ import type { ToolPart } from "@opencode-ai/sdk/v2" import type * as Tool from "@/tool/tool" import type { ApplyPatchTool } from "@/tool/apply_patch" import type { ShellTool as BashTool } from "@/tool/shell" +import type { BrowserExecuteTool } from "@/tool/browser-execute" import type { EditTool } from "@/tool/edit" import type { GlobTool } from "@/tool/glob" import type { GrepTool } from "@/tool/grep" @@ -94,6 +95,7 @@ type ToolPermissionCtx = { type ToolDefs = { invalid: typeof InvalidTool bash: typeof BashTool + browser_execute: typeof BrowserExecuteTool write: typeof WriteTool edit: typeof EditTool apply_patch: typeof ApplyPatchTool @@ -682,6 +684,40 @@ function scrollBashFinal(p: ToolProps): string { return `bash completed (exit ${code})${time ? ` · ${time}` : ""}` } +// Mimic a JS REPL: "> " on the first line, " " on continuations. Skip +// leading/trailing blank lines so a snippet that starts with "\n" doesn't +// render an empty "> " row. Matches the TUI BrowserExecute renderer. +function promptedCode(code: string): string { + const src = code.replace(/^\n+|\n+$/g, "") + if (!src) { + return "" + } + + return src + .split("\n") + .map((line, i) => (i === 0 ? "> " : " ") + line) + .join("\n") +} + +function scrollBrowserExecuteStart(p: ToolProps): string { + const desc = p.input.description || "Browser execute" + const code = promptedCode(p.input.code ?? "") + if (!code) { + return `# ${desc}` + } + + return `# ${desc}\n${code}` +} + +function scrollBrowserExecuteProgress(p: ToolProps): string { + return stripAnsi(p.frame.raw).replace(/^\n+/, "").replace(/\n+$/, "") +} + +function scrollBrowserExecuteFinal(p: ToolProps): string { + const time = span(p.frame.state) + return time ? `browser_execute completed · ${time}` : "browser_execute completed" +} + function scrollReadStart(p: ToolProps): string { const file = toolPath(p.input.filePath) const extra = info(p.frame.input, ["filePath"]) @@ -973,6 +1009,16 @@ function permBash(p: ToolPermissionProps): ToolPermissionInfo { } } +function permBrowserExecute(p: ToolPermissionProps): ToolPermissionInfo { + const title = p.input.description || "Browser execute" + const code = promptedCode(p.input.code ?? "") + return { + icon: "#", + title, + lines: code ? code.split("\n") : p.patterns.map((item) => `- ${item}`), + } +} + function permTask(p: ToolPermissionProps): ToolPermissionInfo { const type = p.input.subagent_type || "general" const desc = p.input.description @@ -1042,6 +1088,19 @@ const TOOL_RULES = { }, permission: permBash, }, + browser_execute: { + view: { + output: true, + final: false, + }, + run: runBrowserExecute, + scroll: { + start: scrollBrowserExecuteStart, + progress: scrollBrowserExecuteProgress, + final: scrollBrowserExecuteFinal, + }, + permission: permBrowserExecute, + }, write: { view: { output: false, @@ -1277,6 +1336,15 @@ function runBash(p: ToolProps): ToolInline { } } +function runBrowserExecute(p: ToolProps): ToolInline { + return { + icon: ">", + title: p.input.description || "Browser execute", + mode: "block", + body: p.frame.status === "completed" ? text(p.frame.state.output).trim() : undefined, + } +} + export function toolView(name?: string): ToolView { return ( rule(name)?.view ?? {