From 3e30068907d98e7e3e9ce01297224ca9b407fd18 Mon Sep 17 00:00:00 2001 From: LukeParkerDev <10430890+Hona@users.noreply.github.com> Date: Thu, 23 Apr 2026 19:46:00 +1000 Subject: [PATCH] refactor: make shell the canonical tool internals --- packages/opencode/src/cli/cmd/agent.ts | 4 +--- packages/opencode/src/cli/cmd/github.ts | 1 - packages/opencode/src/cli/cmd/run.ts | 2 +- .../src/cli/cmd/tui/routes/session/index.tsx | 2 +- packages/opencode/src/session/llm.ts | 3 +-- packages/opencode/src/session/message-v2.ts | 19 +++------------ packages/opencode/src/tool/registry.ts | 2 +- .../opencode/src/tool/{bash.ts => shell.ts} | 18 +++++++------- packages/opencode/src/tool/shell/id.ts | 6 ++--- packages/opencode/src/tool/shell/tool.ts | 1 - packages/opencode/src/tool/task.ts | 3 +-- .../opencode/test/session/message-v2.test.ts | 6 ++--- .../session/session-entry-stepper.test.ts | 24 +++++++++---------- packages/opencode/test/tool/shell.test.ts | 2 +- packages/opencode/test/tool/task.test.ts | 2 +- .../ui/src/components/tool-error-card.tsx | 1 - 16 files changed, 36 insertions(+), 60 deletions(-) rename packages/opencode/src/tool/{bash.ts => shell.ts} (96%) delete mode 100644 packages/opencode/src/tool/shell/tool.ts diff --git a/packages/opencode/src/cli/cmd/agent.ts b/packages/opencode/src/cli/cmd/agent.ts index f8cbae42fc..dee5fea7ac 100644 --- a/packages/opencode/src/cli/cmd/agent.ts +++ b/packages/opencode/src/cli/cmd/agent.ts @@ -12,8 +12,6 @@ import matter from "gray-matter" import { Instance } from "../../project/instance" import { EOL } from "os" import type { Argv } from "yargs" -import { ShellToolID } from "../../tool/shell/id" - type AgentMode = "all" | "primary" | "subagent" const AVAILABLE_TOOLS = ["shell", "read", "write", "edit", "glob", "grep", "webfetch", "task", "todowrite"] @@ -129,7 +127,7 @@ const AgentCreateCommand = cmd({ ...new Set( cliTools .split(",") - .map((t) => ShellToolID.normalize(t.trim())) + .map((t) => t.trim()) .filter(Boolean), ), ] diff --git a/packages/opencode/src/cli/cmd/github.ts b/packages/opencode/src/cli/cmd/github.ts index ba33cbd81e..00291daf9f 100644 --- a/packages/opencode/src/cli/cmd/github.ts +++ b/packages/opencode/src/cli/cmd/github.ts @@ -880,7 +880,6 @@ export const GithubRunCommand = cmd({ const TOOL: Record = { todowrite: ["Todo", UI.Style.TEXT_WARNING_BOLD], shell: ["Shell", UI.Style.TEXT_DANGER_BOLD], - bash: ["Shell", UI.Style.TEXT_DANGER_BOLD], edit: ["Edit", UI.Style.TEXT_SUCCESS_BOLD], glob: ["Glob", UI.Style.TEXT_INFO_BOLD], grep: ["Grep", UI.Style.TEXT_INFO_BOLD], diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index f63d762ae0..06cfda542d 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -23,7 +23,7 @@ import { CodeSearchTool } from "../../tool/codesearch" import { WebSearchTool } from "../../tool/websearch" import { TaskTool } from "../../tool/task" import { SkillTool } from "../../tool/skill" -import { ShellTool } from "../../tool/shell/tool" +import { ShellTool } from "../../tool/shell" import { ShellToolID } from "../../tool/shell/id" import { TodoWriteTool } from "../../tool/todo" import { Locale } from "../../util" 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 86ad2ca986..31f3561373 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -37,7 +37,7 @@ import { Locale } from "@/util" import type { Tool } from "@/tool" import type { ReadTool } from "@/tool/read" import type { WriteTool } from "@/tool/write" -import { ShellTool } from "@/tool/shell/tool" +import { ShellTool } from "@/tool/shell" import { ShellToolID } from "@/tool/shell/id" import type { GlobTool } from "@/tool/glob" import { TodoWriteTool } from "@/tool/todo" diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts index 0824cba426..848d06c888 100644 --- a/packages/opencode/src/session/llm.ts +++ b/packages/opencode/src/session/llm.ts @@ -22,7 +22,6 @@ import { Auth } from "@/auth" import { Installation } from "@/installation" import { InstallationVersion } from "@/installation/version" import { EffectBridge } from "@/effect" -import { ShellToolID } from "@/tool/shell/id" import * as Option from "effect/Option" import * as OtelTracer from "@effect/opentelemetry/Tracer" @@ -208,7 +207,7 @@ const live: Layer.Layer< input.model.api.id.toLowerCase().includes("litellm") const repair = (toolName: string) => { - const next = ShellToolID.normalize(toolName.toLowerCase()) + const next = toolName.toLowerCase() if (!tools[next]) return return next } diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index 2b66fc285c..c6d2c2be1f 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -16,7 +16,6 @@ import type { SystemError } from "bun" import type { Provider } from "@/provider" import { ModelID, ProviderID } from "@/provider/schema" import { Effect, Schema, Types } from "effect" -import { ShellToolID } from "@/tool/shell/id" import { zod, ZodOverride } from "@/util/effect-zod" import { withStatics } from "@/util/schema" import { namedSchemaError } from "@/util/named-schema-error" @@ -443,17 +442,6 @@ export type Part = | RetryPart | CompactionPart -function normalizeTool(tool: string) { - return ShellToolID.normalize(tool) -} - -function normalizePart(part: T): T { - if (part.type !== "tool") return part - const tool = normalizeTool(part.tool) - if (tool === part.tool) return part - return { ...part, tool } as T -} - // Errors are still NamedError-based Zod; bridge via ZodOverride so the derived // Zod + JSON Schema emit the original discriminatedUnion shape. Migrating the // error classes to Schema.TaggedErrorClass is a separate slice. @@ -673,12 +661,12 @@ const info = (row: typeof MessageTable.$inferSelect) => }) as Info const part = (row: typeof PartTable.$inferSelect) => - normalizePart(({ + ({ ...row.data, id: row.id, sessionID: row.session_id, messageID: row.message_id, - } as Part)) + } as Part) const older = (row: Cursor) => or(lt(MessageTable.time_created, row.time), and(eq(MessageTable.time_created, row.time), lt(MessageTable.id, row.id))) @@ -843,8 +831,7 @@ export const toModelMessagesEffect = Effect.fnUntraced(function* ( role: "assistant", parts: [], } - for (const raw of msg.parts) { - const part = normalizePart(raw) + for (const part of msg.parts) { if (part.type === "text") assistantMessage.parts.push({ type: "text", diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index aa27e900e5..0c1289a78b 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -1,7 +1,7 @@ import { PlanExitTool } from "./plan" import { Session } from "../session" import { QuestionTool } from "./question" -import { ShellTool } from "./shell/tool" +import { ShellTool } from "./shell" import { EditTool } from "./edit" import { GlobTool } from "./glob" import { GrepTool } from "./grep" diff --git a/packages/opencode/src/tool/bash.ts b/packages/opencode/src/tool/shell.ts similarity index 96% rename from packages/opencode/src/tool/bash.ts rename to packages/opencode/src/tool/shell.ts index 8bf86303dd..9523629959 100644 --- a/packages/opencode/src/tool/bash.ts +++ b/packages/opencode/src/tool/shell.ts @@ -253,13 +253,13 @@ function tail(text: string, maxLines: number, maxBytes: number) { } } -const parse = Effect.fn("BashTool.parse")(function* (command: string, ps: boolean) { +const parse = Effect.fn("ShellTool.parse")(function* (command: string, ps: boolean) { const tree = yield* Effect.promise(() => parser().then((p) => (ps ? p.ps : p.bash).parse(command))) if (!tree) throw new Error("Failed to parse command") return tree.rootNode }) -const ask = Effect.fn("BashTool.ask")(function* (ctx: Tool.Context, scan: Scan) { +const ask = Effect.fn("ShellTool.ask")(function* (ctx: Tool.Context, scan: Scan) { if (scan.dirs.size > 0) { const globs = Array.from(scan.dirs).map((dir) => { if (process.platform === "win32") return AppFileSystem.normalizePathPattern(path.join(dir, "*")) @@ -336,7 +336,7 @@ export const ShellTool = Tool.define( const trunc = yield* Truncate.Service const plugin = yield* Plugin.Service - const cygpath = Effect.fn("BashTool.cygpath")(function* (shell: string, text: string) { + const cygpath = Effect.fn("ShellTool.cygpath")(function* (shell: string, text: string) { const lines = yield* spawner .lines(ChildProcess.make(shell, ["-lc", 'cygpath -w -- "$1"', "_", text])) .pipe(Effect.catch(() => Effect.succeed([] as string[]))) @@ -345,7 +345,7 @@ export const ShellTool = Tool.define( return AppFileSystem.normalizePath(file) }) - const resolvePath = Effect.fn("BashTool.resolvePath")(function* (text: string, root: string, shell: string) { + const resolvePath = Effect.fn("ShellTool.resolvePath")(function* (text: string, root: string, shell: string) { if (process.platform === "win32") { if (Shell.posix(shell) && text.startsWith("/") && AppFileSystem.windowsPath(text) === text) { const file = yield* cygpath(shell, text) @@ -356,7 +356,7 @@ export const ShellTool = Tool.define( return path.resolve(root, text) }) - const argPath = Effect.fn("BashTool.argPath")(function* (arg: string, cwd: string, ps: boolean, shell: string) { + const argPath = Effect.fn("ShellTool.argPath")(function* (arg: string, cwd: string, ps: boolean, shell: string) { const text = ps ? expand(arg, cwd, shell) : home(unquote(arg)) const file = text && prefix(text) if (!file || dynamic(file, ps)) return @@ -365,7 +365,7 @@ export const ShellTool = Tool.define( return yield* resolvePath(next, cwd, shell) }) - const collect = Effect.fn("BashTool.collect")(function* (root: Node, cwd: string, ps: boolean, shell: string) { + const collect = Effect.fn("ShellTool.collect")(function* (root: Node, cwd: string, ps: boolean, shell: string) { const scan: Scan = { dirs: new Set(), patterns: new Set(), @@ -396,7 +396,7 @@ export const ShellTool = Tool.define( return scan }) - const shellEnv = Effect.fn("BashTool.shellEnv")(function* (ctx: Tool.Context, cwd: string) { + const shellEnv = Effect.fn("ShellTool.shellEnv")(function* (ctx: Tool.Context, cwd: string) { const extra = yield* plugin.trigger( "shell.env", { cwd, sessionID: ctx.sessionID, callID: ctx.callID }, @@ -408,7 +408,7 @@ export const ShellTool = Tool.define( } }) - const run = Effect.fn("BashTool.run")(function* ( + const run = Effect.fn("ShellTool.run")(function* ( input: { shell: string name: string @@ -644,5 +644,3 @@ export const ShellTool = Tool.define( }) }), ) - -export const BashTool = ShellTool diff --git a/packages/opencode/src/tool/shell/id.ts b/packages/opencode/src/tool/shell/id.ts index 2808d99ee8..60673dd698 100644 --- a/packages/opencode/src/tool/shell/id.ts +++ b/packages/opencode/src/tool/shell/id.ts @@ -21,12 +21,10 @@ export namespace ShellKind { export namespace ShellToolID { export const id = "shell" export const legacy = "bash" - export type ID = typeof id | typeof legacy - - const tool = new Set([id, legacy]) + export type ID = typeof id export function has(value: string): value is ID { - return tool.has(value) + return value === id } export function normalize(value: string) { diff --git a/packages/opencode/src/tool/shell/tool.ts b/packages/opencode/src/tool/shell/tool.ts deleted file mode 100644 index b3af7377a8..0000000000 --- a/packages/opencode/src/tool/shell/tool.ts +++ /dev/null @@ -1 +0,0 @@ -export { ShellTool } from "../bash" diff --git a/packages/opencode/src/tool/task.ts b/packages/opencode/src/tool/task.ts index 05efe69b63..e525a25bcd 100644 --- a/packages/opencode/src/tool/task.ts +++ b/packages/opencode/src/tool/task.ts @@ -8,7 +8,6 @@ import { Agent } from "../agent/agent" import type { SessionPrompt } from "../session/prompt" import { Config } from "../config" import { Effect } from "effect" -import { ShellToolID } from "./shell/id" export interface TaskPromptOps { cancel(sessionID: SessionID): void @@ -40,7 +39,7 @@ export const TaskTool = Tool.define( const run = Effect.fn("TaskTool.execute")(function* (params: z.infer, ctx: Tool.Context) { const cfg = yield* config.get() - const primaryTools = (cfg.experimental?.primary_tools ?? []).map((item) => ShellToolID.normalize(item)) + const primaryTools = cfg.experimental?.primary_tools ?? [] if (!ctx.extra?.bypassAgentCheck) { yield* ctx.ask({ diff --git a/packages/opencode/test/session/message-v2.test.ts b/packages/opencode/test/session/message-v2.test.ts index b5825ab36f..3d0112ba51 100644 --- a/packages/opencode/test/session/message-v2.test.ts +++ b/packages/opencode/test/session/message-v2.test.ts @@ -607,12 +607,12 @@ describe("session.message-v2.toModelMessage", () => { ...basePart(assistantID, "a1"), type: "tool", callID: "call-1", - tool: "bash", + tool: "shell", state: { status: "completed", input: { cmd: "ls" }, output: "abcdefghij", - title: "Bash", + title: "Shell", metadata: {}, time: { start: 0, end: 1 }, }, @@ -755,7 +755,7 @@ describe("session.message-v2.toModelMessage", () => { ...basePart(assistantID, "a1"), type: "tool", callID: "call-1", - tool: "bash", + tool: "shell", state: { status: "error", input: { command: "for i in {1..20}; do print -- $RANDOM; sleep 1; done" }, diff --git a/packages/opencode/test/session/session-entry-stepper.test.ts b/packages/opencode/test/session/session-entry-stepper.test.ts index defce40c14..014f9ed294 100644 --- a/packages/opencode/test/session/session-entry-stepper.test.ts +++ b/packages/opencode/test/session/session-entry-stepper.test.ts @@ -414,13 +414,13 @@ describe("session-entry-stepper", () => { (callID, title, input, output, metadata, attachments, parts) => { const next = run( [ - SessionEvent.Tool.Input.Started.create({ callID, name: "bash", timestamp: time(1) }), + SessionEvent.Tool.Input.Started.create({ callID, name: "shell", timestamp: time(1) }), ...parts.map((x, i) => SessionEvent.Tool.Input.Delta.create({ callID, delta: x, timestamp: time(i + 2) }), ), SessionEvent.Tool.Called.create({ callID, - tool: "bash", + tool: "shell", input, provider: { executed: true }, timestamp: time(parts.length + 2), @@ -459,10 +459,10 @@ describe("session-entry-stepper", () => { FastCheck.property(word, dict, word, maybe(dict), (callID, input, error, metadata) => { const next = run( [ - SessionEvent.Tool.Input.Started.create({ callID, name: "bash", timestamp: time(1) }), + SessionEvent.Tool.Input.Started.create({ callID, name: "shell", timestamp: time(1) }), SessionEvent.Tool.Called.create({ callID, - tool: "bash", + tool: "shell", input, provider: { executed: true }, timestamp: time(2), @@ -496,7 +496,7 @@ describe("session-entry-stepper", () => { FastCheck.property(word, word, (callID, title) => { const next = run( [ - SessionEvent.Tool.Input.Started.create({ callID, name: "bash", timestamp: time(1) }), + SessionEvent.Tool.Input.Started.create({ callID, name: "shell", timestamp: time(1) }), SessionEvent.Tool.Success.create({ callID, title, @@ -691,10 +691,10 @@ describe("session-entry-stepper", () => { SessionEvent.Reasoning.Started.create({ timestamp: time(2) }), ...reason.map((x, i) => SessionEvent.Reasoning.Delta.create({ delta: x, timestamp: time(i + 3) })), SessionEvent.Reasoning.Ended.create({ text: end, timestamp: time(reason.length + 3) }), - SessionEvent.Tool.Input.Started.create({ callID, name: "bash", timestamp: time(reason.length + 4) }), + SessionEvent.Tool.Input.Started.create({ callID, name: "shell", timestamp: time(reason.length + 4) }), SessionEvent.Tool.Called.create({ callID, - tool: "bash", + tool: "shell", input, provider: { executed: true }, timestamp: time(reason.length + 5), @@ -771,10 +771,10 @@ describe("session-entry-stepper", () => { FastCheck.property(dict, dict, word, word, (a, b, title, error) => { const next = run( [ - SessionEvent.Tool.Input.Started.create({ callID: "a", name: "bash", timestamp: time(1) }), + SessionEvent.Tool.Input.Started.create({ callID: "a", name: "shell", timestamp: time(1) }), SessionEvent.Tool.Called.create({ callID: "a", - tool: "bash", + tool: "shell", input: a, provider: { executed: true }, timestamp: time(2), @@ -789,7 +789,7 @@ describe("session-entry-stepper", () => { SessionEvent.Tool.Input.Started.create({ callID: "b", name: "grep", timestamp: time(4) }), SessionEvent.Tool.Called.create({ callID: "b", - tool: "bash", + tool: "shell", input: b, provider: { executed: true }, timestamp: time(5), @@ -827,13 +827,13 @@ describe("session-entry-stepper", () => { FastCheck.property(dict, dict, word, word, text, text, (a, b, titleA, titleB, deltaA, deltaB) => { const next = run( [ - SessionEvent.Tool.Input.Started.create({ callID: "a", name: "bash", timestamp: time(1) }), + SessionEvent.Tool.Input.Started.create({ callID: "a", name: "shell", timestamp: time(1) }), SessionEvent.Tool.Input.Started.create({ callID: "b", name: "grep", timestamp: time(2) }), SessionEvent.Tool.Input.Delta.create({ callID: "a", delta: deltaA, timestamp: time(3) }), SessionEvent.Tool.Input.Delta.create({ callID: "b", delta: deltaB, timestamp: time(4) }), SessionEvent.Tool.Called.create({ callID: "a", - tool: "bash", + tool: "shell", input: a, provider: { executed: true }, timestamp: time(5), diff --git a/packages/opencode/test/tool/shell.test.ts b/packages/opencode/test/tool/shell.test.ts index b14cad7e01..b06bee2ddc 100644 --- a/packages/opencode/test/tool/shell.test.ts +++ b/packages/opencode/test/tool/shell.test.ts @@ -4,7 +4,7 @@ import os from "os" import path from "path" import { Shell } from "../../src/shell/shell" import { ShellToolID } from "../../src/tool/shell/id" -import { ShellTool } from "../../src/tool/shell/tool" +import { ShellTool } from "../../src/tool/shell" import { Instance } from "../../src/project/instance" import { Filesystem } from "../../src/util" import { tmpdir } from "../fixture/fixture" diff --git a/packages/opencode/test/tool/task.test.ts b/packages/opencode/test/tool/task.test.ts index 06f28bde25..faf73d9edf 100644 --- a/packages/opencode/test/tool/task.test.ts +++ b/packages/opencode/test/tool/task.test.ts @@ -378,7 +378,7 @@ describe("tool.task", () => { }, }, experimental: { - primary_tools: ["bash", "read"], + primary_tools: ["shell", "read"], }, }, }, diff --git a/packages/ui/src/components/tool-error-card.tsx b/packages/ui/src/components/tool-error-card.tsx index 1ee7d3c360..3b0b293319 100644 --- a/packages/ui/src/components/tool-error-card.tsx +++ b/packages/ui/src/components/tool-error-card.tsx @@ -35,7 +35,6 @@ export function ToolErrorCard(props: ToolErrorCardProps) { websearch: "ui.tool.websearch", codesearch: "ui.tool.codesearch", shell: "ui.tool.shell", - bash: "ui.tool.shell", apply_patch: "ui.tool.patch", question: "ui.tool.questions", }