From 2eb9ae4d34340e298ade122cc0051a464615561d Mon Sep 17 00:00:00 2001 From: LukeParkerDev <10430890+Hona@users.noreply.github.com> Date: Fri, 3 Apr 2026 14:56:40 +1000 Subject: [PATCH] refactor(shell): centralize shell tool identity Move shell tool ID checks behind shared helpers so runtime code and tests stop duplicating bash, pwsh, and powershell branches. This keeps shell-specific behavior aligned across consumers and makes follow-on shell changes less error-prone. --- .../app/e2e/prompt/prompt-history.spec.ts | 12 +----- packages/app/e2e/prompt/prompt-shell.spec.ts | 11 +---- packages/app/e2e/utils.ts | 10 ++++- packages/opencode/src/acp/agent.ts | 41 +++++++++---------- packages/opencode/src/agent/agent.ts | 2 - packages/opencode/src/cli/cmd/agent.ts | 5 +-- packages/opencode/src/cli/cmd/run.ts | 4 +- .../src/cli/cmd/tui/routes/session/index.tsx | 3 +- .../cli/cmd/tui/routes/session/permission.tsx | 3 +- packages/opencode/src/session/prompt.ts | 3 +- packages/opencode/src/tool/registry.ts | 8 ++-- packages/opencode/src/tool/shell/arity.ts | 10 ++--- packages/opencode/src/tool/shell/id.ts | 19 +++++++++ packages/opencode/src/tool/shell/parser.ts | 5 ++- packages/opencode/src/tool/shell/runner.ts | 3 +- packages/opencode/src/tool/shell/util.ts | 3 +- .../test/session/snapshot-tool-race.test.ts | 13 +++--- packages/opencode/test/tool/shell.test.ts | 20 ++++----- packages/ui/src/components/message-part.tsx | 16 ++++---- 19 files changed, 99 insertions(+), 92 deletions(-) create mode 100644 packages/opencode/src/tool/shell/id.ts diff --git a/packages/app/e2e/prompt/prompt-history.spec.ts b/packages/app/e2e/prompt/prompt-history.spec.ts index 55cb0c9aa3..0ecac65ef4 100644 --- a/packages/app/e2e/prompt/prompt-history.spec.ts +++ b/packages/app/e2e/prompt/prompt-history.spec.ts @@ -1,20 +1,12 @@ -import type { ToolPart } from "@opencode-ai/sdk/v2/client" import type { Page } from "@playwright/test" import { test, expect } from "../fixtures" import { assistantText } from "../actions" import { promptSelector } from "../selectors" -import { createSdk } from "../utils" +import { createSdk, isShell } from "../utils" const text = (value: string | null) => (value ?? "").replace(/\u200B/g, "").trim() type Sdk = ReturnType -const isBash = (part: unknown): part is ToolPart => { - if (!part || typeof part !== "object") return false - if (!("type" in part) || part.type !== "tool") return false - if (!("tool" in part) || part.tool !== "bash") return false - return "state" in part -} - async function wait(page: Page, value: string) { await expect.poll(async () => text(await page.locator(promptSelector).textContent())).toBe(value) } @@ -31,7 +23,7 @@ async function shell(sdk: Sdk, sessionID: string, cmd: string, token: string) { const part = messages .filter((item) => item.info.role === "assistant") .flatMap((item) => item.parts) - .filter(isBash) + .filter(isShell) .find((item) => item.state.input?.command === cmd && item.state.status === "completed") if (!part || part.state.status !== "completed") return diff --git a/packages/app/e2e/prompt/prompt-shell.spec.ts b/packages/app/e2e/prompt/prompt-shell.spec.ts index d81f1d4c40..366011bdef 100644 --- a/packages/app/e2e/prompt/prompt-shell.spec.ts +++ b/packages/app/e2e/prompt/prompt-shell.spec.ts @@ -1,13 +1,6 @@ -import type { ToolPart } from "@opencode-ai/sdk/v2/client" import { test, expect } from "../fixtures" import { withSession } from "../actions" - -const isBash = (part: unknown): part is ToolPart => { - if (!part || typeof part !== "object") return false - if (!("type" in part) || part.type !== "tool") return false - if (!("tool" in part) || part.tool !== "bash") return false - return "state" in part -} +import { isShell } from "../utils" async function setAutoAccept(page: Parameters[0]["page"], enabled: boolean) { const button = page.locator('[data-action="prompt-permissions"]').first() @@ -42,7 +35,7 @@ test("shell mode runs a command in the project directory", async ({ page, projec if (!msg) return const part = msg.parts - .filter(isBash) + .filter(isShell) .find((item) => item.state.input?.command === cmd && item.state.status === "completed") if (!part || part.state.status !== "completed") return diff --git a/packages/app/e2e/utils.ts b/packages/app/e2e/utils.ts index 17a8785664..9e4d13e0ad 100644 --- a/packages/app/e2e/utils.ts +++ b/packages/app/e2e/utils.ts @@ -1,4 +1,4 @@ -import { createOpencodeClient } from "@opencode-ai/sdk/v2/client" +import { createOpencodeClient, type ToolPart } from "@opencode-ai/sdk/v2/client" import { base64Encode, checksum } from "@opencode-ai/util/encode" export const serverHost = process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1" @@ -18,6 +18,7 @@ const serverLabels = (() => { export const serverNames = [...new Set(serverLabels)] export const serverUrls = serverNames.map((name) => `http://${name}`) +const shell = new Set(["bash", "pwsh", "powershell"]) const escape = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") @@ -30,6 +31,13 @@ export function createSdk(directory?: string, baseUrl = serverUrl) { return createOpencodeClient({ baseUrl, directory, throwOnError: true }) } +export function isShell(part: unknown): part is ToolPart { + if (!part || typeof part !== "object") return false + if (!("type" in part) || part.type !== "tool") return false + if (!("tool" in part) || typeof part.tool !== "string" || !shell.has(part.tool)) return false + return "state" in part +} + export async function resolveDirectory(directory: string, baseUrl = serverUrl) { return createSdk(directory, baseUrl) .path.get() diff --git a/packages/opencode/src/acp/agent.ts b/packages/opencode/src/acp/agent.ts index f5b264cbe5..a2c93cbf5a 100644 --- a/packages/opencode/src/acp/agent.ts +++ b/packages/opencode/src/acp/agent.ts @@ -38,6 +38,7 @@ import { Provider } from "../provider/provider" import { ModelID, ProviderID } from "../provider/schema" import { Agent as AgentModule } from "../agent/agent" import { Installation } from "@/installation" +import { ShellTool } from "@/tool/shell/id" import { MessageV2 } from "@/session/message-v2" import { Config } from "@/config/config" import { Todo } from "@/session/todo" @@ -138,7 +139,7 @@ export namespace ACP { private sessionManager: ACPSessionManager private eventAbort = new AbortController() private eventStarted = false - private bashSnapshots = new Map() + private shellSnapshots = new Map() private toolStarts = new Set() private permissionQueues = new Map>() private permissionOptions: PermissionOption[] = [ @@ -277,16 +278,16 @@ export namespace ACP { switch (part.state.status) { case "pending": - this.bashSnapshots.delete(part.callID) + this.shellSnapshots.delete(part.callID) return case "running": - const output = this.bashOutput(part) + const output = this.shellOutput(part) const content: ToolCallContent[] = [] if (output) { const hash = Hash.fast(output) - if (part.tool === "bash" || part.tool === "pwsh" || part.tool === "powershell") { - if (this.bashSnapshots.get(part.callID) === hash) { + if (ShellTool.has(part.tool)) { + if (this.shellSnapshots.get(part.callID) === hash) { await this.connection .sessionUpdate({ sessionId, @@ -305,7 +306,7 @@ export namespace ACP { }) return } - this.bashSnapshots.set(part.callID, hash) + this.shellSnapshots.set(part.callID, hash) } content.push({ type: "content", @@ -336,7 +337,7 @@ export namespace ACP { case "completed": { this.toolStarts.delete(part.callID) - this.bashSnapshots.delete(part.callID) + this.shellSnapshots.delete(part.callID) const kind = toToolKind(part.tool) const content: ToolCallContent[] = [ { @@ -417,7 +418,7 @@ export namespace ACP { } case "error": this.toolStarts.delete(part.callID) - this.bashSnapshots.delete(part.callID) + this.shellSnapshots.delete(part.callID) await this.connection .sessionUpdate({ sessionId, @@ -832,10 +833,10 @@ export namespace ACP { await this.toolStart(sessionId, part) switch (part.state.status) { case "pending": - this.bashSnapshots.delete(part.callID) + this.shellSnapshots.delete(part.callID) break case "running": - const output = this.bashOutput(part) + const output = this.shellOutput(part) const runningContent: ToolCallContent[] = [] if (output) { runningContent.push({ @@ -866,7 +867,7 @@ export namespace ACP { break case "completed": this.toolStarts.delete(part.callID) - this.bashSnapshots.delete(part.callID) + this.shellSnapshots.delete(part.callID) const kind = toToolKind(part.tool) const content: ToolCallContent[] = [ { @@ -946,7 +947,7 @@ export namespace ACP { break case "error": this.toolStarts.delete(part.callID) - this.bashSnapshots.delete(part.callID) + this.shellSnapshots.delete(part.callID) await this.connection .sessionUpdate({ sessionId, @@ -1100,8 +1101,8 @@ export namespace ACP { } } - private bashOutput(part: ToolPart) { - if (part.tool !== "bash" && part.tool !== "pwsh" && part.tool !== "powershell") return + private shellOutput(part: ToolPart) { + if (!ShellTool.has(part.tool)) return if (!("metadata" in part.state) || !part.state.metadata || typeof part.state.metadata !== "object") return const output = part.state.metadata["output"] if (typeof output !== "string") return @@ -1502,11 +1503,9 @@ export namespace ACP { function toToolKind(toolName: string): ToolKind { const tool = toolName.toLocaleLowerCase() + if (ShellTool.has(tool)) return "execute" + switch (tool) { - case "bash": - case "pwsh": - case "powershell": - return "execute" case "webfetch": return "fetch" @@ -1532,6 +1531,8 @@ export namespace ACP { function toLocations(toolName: string, input: Record): { path: string }[] { const tool = toolName.toLocaleLowerCase() + if (ShellTool.has(tool)) return [] + switch (tool) { case "read": case "edit": @@ -1540,10 +1541,6 @@ export namespace ACP { case "glob": case "grep": return input["path"] ? [{ path: input["path"] }] : [] - case "bash": - case "pwsh": - case "powershell": - return [] case "list": return input["path"] ? [{ path: input["path"] }] : [] default: diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index 02592b0d6a..0c6fe6ec91 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -168,8 +168,6 @@ export namespace Agent { glob: "allow", list: "allow", bash: "allow", - pwsh: "allow", - powershell: "allow", webfetch: "allow", websearch: "allow", codesearch: "allow", diff --git a/packages/opencode/src/cli/cmd/agent.ts b/packages/opencode/src/cli/cmd/agent.ts index a8e73c90b7..d0777d3ef7 100644 --- a/packages/opencode/src/cli/cmd/agent.ts +++ b/packages/opencode/src/cli/cmd/agent.ts @@ -9,15 +9,14 @@ import fs from "fs/promises" import { Filesystem } from "../../util/filesystem" import matter from "gray-matter" import { Instance } from "../../project/instance" +import { ShellTool } from "../../tool/shell/id" import { EOL } from "os" import type { Argv } from "yargs" type AgentMode = "all" | "primary" | "subagent" const AVAILABLE_TOOLS = [ - "bash", - "pwsh", - "powershell", + ...ShellTool.ids, "read", "write", "edit", diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index 615cbf6494..de09719933 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -25,6 +25,7 @@ import { WebSearchTool } from "../../tool/websearch" import { TaskTool } from "../../tool/task" import { SkillTool } from "../../tool/skill" import { BashTool } from "../../tool/shell/bash" +import { ShellTool } from "../../tool/shell/id" import { TodoWriteTool } from "../../tool/todo" import { Locale } from "../../util/locale" @@ -411,8 +412,7 @@ export const RunCommand = cmd({ async function execute(sdk: OpencodeClient) { function tool(part: ToolPart) { try { - if (part.tool === "bash" || part.tool === "pwsh" || part.tool === "powershell") - return bash(props(part)) + if (ShellTool.has(part.tool)) return bash(props(part)) if (part.tool === "glob") return glob(props(part)) if (part.tool === "grep") return grep(props(part)) if (part.tool === "list") return list(props(part)) 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 a9129bb9f4..e47b50ac33 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -36,6 +36,7 @@ import type { Tool } from "@/tool/tool" import type { ReadTool } from "@/tool/read" import type { WriteTool } from "@/tool/write" import { BashTool } from "@/tool/shell/bash" +import { ShellTool } from "@/tool/shell/id" import type { GlobTool } from "@/tool/glob" import { TodoWriteTool } from "@/tool/todo" import type { GrepTool } from "@/tool/grep" @@ -1519,7 +1520,7 @@ function ToolPart(props: { last: boolean; part: ToolPart; message: AssistantMess return ( - + diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx index 9147da78aa..a5723e1d7a 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx @@ -14,6 +14,7 @@ import { LANGUAGE_EXTENSIONS } from "@/lsp/language" import { Keybind } from "@/util/keybind" import { Locale } from "@/util/locale" import { Global } from "@/global" +import { ShellTool } from "@/tool/shell/id" import { useDialog } from "../../ui/dialog" import { getScrollAcceleration } from "../../util/scroll" import { useTuiConfig } from "../../context/tui-config" @@ -283,7 +284,7 @@ export function PermissionPrompt(props: { request: PermissionRequest }) { } } - if (permission === "bash" || permission === "pwsh" || permission === "powershell") { + if (ShellTool.has(permission)) { const title = typeof data.description === "string" && data.description ? data.description : "Shell command" const command = typeof data.command === "string" ? data.command : "" diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 115c93547b..823bb971b8 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -43,6 +43,7 @@ import { Permission } from "@/permission" import { SessionStatus } from "./status" import { LLM } from "./llm" import { Shell } from "@/shell/shell" +import { ShellTool } from "@/tool/shell/id" import { AppFileSystem } from "@/filesystem" import { Truncate } from "@/tool/truncate" import { decodeDataUrl } from "@/util/data-url" @@ -791,7 +792,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the yield* sessions.updateMessage(msg) const sh = Shell.preferred() const name = Shell.name(sh) - const tool = name === "pwsh" ? "pwsh" : name === "powershell" ? "powershell" : "bash" + const tool = ShellTool.from(name) const part: MessageV2.ToolPart = { type: "tool", id: PartID.ascending(), diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index 6dce6d75e1..15f84a781a 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -32,6 +32,7 @@ import { Effect, Layer, ServiceMap } from "effect" import { InstanceState } from "@/effect/instance-state" import { makeRuntime } from "@/effect/run-service" import { BashTool } from "./shell/bash" +import { ShellTool } from "./shell/id" import { PwshTool } from "./shell/pwsh" import { PowershellTool } from "./shell/powershell" import { Shell } from "@/shell/shell" @@ -39,6 +40,7 @@ import { Env } from "../env" export namespace ToolRegistry { const log = Log.create({ service: "tool.registry" }) + const shells = { bash: BashTool, pwsh: PwshTool, powershell: PowershellTool } as const type State = { custom: Tool.Info[] @@ -118,14 +120,12 @@ export namespace ToolRegistry { const all = Effect.fn("ToolRegistry.all")(function* (custom: Tool.Info[]) { const cfg = yield* config.get() const question = ["app", "cli", "desktop"].includes(Flag.OPENCODE_CLIENT) || Flag.OPENCODE_ENABLE_QUESTION_TOOL - - const shellName = Shell.name(Shell.acceptable()) - const ActiveShellTool = shellName === "pwsh" ? PwshTool : shellName === "powershell" ? PowershellTool : BashTool + const active = shells[ShellTool.from(Shell.name(Shell.acceptable()))] return [ InvalidTool, ...(question ? [QuestionTool] : []), - ActiveShellTool, + active, ReadTool, GlobTool, GrepTool, diff --git a/packages/opencode/src/tool/shell/arity.ts b/packages/opencode/src/tool/shell/arity.ts index 3ec40b6646..c97d4b8eec 100644 --- a/packages/opencode/src/tool/shell/arity.ts +++ b/packages/opencode/src/tool/shell/arity.ts @@ -1,10 +1,8 @@ +import { ShellTool } from "./id" + export namespace ShellArity { - export function prefix(tokens: string[], shellType: "bash" | "pwsh" | "powershell") { - if ( - (shellType === "pwsh" || shellType === "powershell") && - tokens.length > 0 && - /^[a-z]+-[a-z]+$/i.test(tokens[0]) - ) { + export function prefix(tokens: string[], shellType: ShellTool.ID) { + if (ShellTool.powershell(shellType) && tokens.length > 0 && /^[a-z]+-[a-z]+$/i.test(tokens[0])) { return [tokens[0]] } for (let len = tokens.length; len > 0; len--) { diff --git a/packages/opencode/src/tool/shell/id.ts b/packages/opencode/src/tool/shell/id.ts new file mode 100644 index 0000000000..0ea57064cf --- /dev/null +++ b/packages/opencode/src/tool/shell/id.ts @@ -0,0 +1,19 @@ +export namespace ShellTool { + export const ids = ["bash", "pwsh", "powershell"] as const + export type ID = (typeof ids)[number] + + const shell = new Set(ids) + const ps = new Set(["pwsh", "powershell"]) + + export function has(value: string): value is ID { + return shell.has(value) + } + + export function from(value: string): ID { + return has(value) ? value : "bash" + } + + export function powershell(value: string) { + return ps.has(value) + } +} diff --git a/packages/opencode/src/tool/shell/parser.ts b/packages/opencode/src/tool/shell/parser.ts index 6260ac6042..3860f65ea5 100644 --- a/packages/opencode/src/tool/shell/parser.ts +++ b/packages/opencode/src/tool/shell/parser.ts @@ -1,6 +1,7 @@ import type { Node } from "web-tree-sitter" import { lazy } from "@/util/lazy" import { resolveWasm, resolvePath, unquote, home, expand, type Scan, type Part } from "./util" +import { ShellTool } from "./id" import { Instance } from "@/project/instance" import { Filesystem } from "@/util/filesystem" import path from "path" @@ -160,9 +161,9 @@ export namespace ShellParser { command: string cwd: string shell: string - shellType: "bash" | "pwsh" | "powershell" + shellType: ShellTool.ID }): Promise { - const isPwsh = opts.shellType === "pwsh" || opts.shellType === "powershell" + const isPwsh = ShellTool.powershell(opts.shellType) const parsers = await getParser() const parser = isPwsh ? parsers.ps : parsers.bash diff --git a/packages/opencode/src/tool/shell/runner.ts b/packages/opencode/src/tool/shell/runner.ts index 2adbf77b14..b86d39cecb 100644 --- a/packages/opencode/src/tool/shell/runner.ts +++ b/packages/opencode/src/tool/shell/runner.ts @@ -2,6 +2,7 @@ import { spawn } from "child_process" import { Shell } from "@/shell/shell" import { Tool } from "../tool" import { Plugin } from "@/plugin" +import { ShellTool } from "./id" const MAX_METADATA_LENGTH = 30_000 @@ -27,7 +28,7 @@ exit 1` } export function launch(shell: string, name: string, command: string, cwd: string, env: NodeJS.ProcessEnv) { - if (process.platform === "win32" && (name === "powershell" || name === "pwsh")) { + if (process.platform === "win32" && ShellTool.powershell(name)) { return spawn(shell, ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", preserveExitCode(command)], { cwd, env, diff --git a/packages/opencode/src/tool/shell/util.ts b/packages/opencode/src/tool/shell/util.ts index b1fb167fbd..a09289235c 100644 --- a/packages/opencode/src/tool/shell/util.ts +++ b/packages/opencode/src/tool/shell/util.ts @@ -108,12 +108,13 @@ export function formatShellDescription( import z from "zod" import DESCRIPTION from "./shell.txt" +import { ShellTool } from "./id" import { Log } from "@/util/log" import { Flag } from "@/flag/flag" import { ShellParser } from "./parser" import { ShellRunner } from "./runner" -export type ShellType = "bash" | "pwsh" | "powershell" +export type ShellType = ShellTool.ID const DEFAULT_TIMEOUT = Flag.OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS || 2 * 60 * 1000 diff --git a/packages/opencode/test/session/snapshot-tool-race.test.ts b/packages/opencode/test/session/snapshot-tool-race.test.ts index 4f4376e341..cf9f340e98 100644 --- a/packages/opencode/test/session/snapshot-tool-race.test.ts +++ b/packages/opencode/test/session/snapshot-tool-race.test.ts @@ -16,10 +16,12 @@ import { Effect } from "effect" import fs from "fs/promises" import path from "path" import { Session } from "../../src/session" +import { Shell } from "../../src/shell/shell" import { LLM } from "../../src/session/llm" import { SessionPrompt } from "../../src/session/prompt" import { SessionSummary } from "../../src/session/summary" import { MessageV2 } from "../../src/session/message-v2" +import { ShellTool } from "../../src/tool/shell/id" import { Log } from "../../src/util/log" import { provideTmpdirServer } from "../fixture/fixture" import { testEffect } from "../lib/effect" @@ -42,7 +44,6 @@ import { SessionCompaction } from "../../src/session/compaction" import { Instruction } from "../../src/session/instruction" import { SessionProcessor } from "../../src/session/processor" import { SessionStatus } from "../../src/session/status" -import { Shell } from "../../src/shell/shell" import { Snapshot } from "../../src/snapshot" import { ToolRegistry } from "../../src/tool/registry" import { Truncate } from "../../src/tool/truncate" @@ -183,13 +184,15 @@ it.live("tool execution produces non-empty session diff (snapshot race)", () => permission: [{ permission: "*", pattern: "*", action: "allow" }], }) - // Use bash tool (always registered) to create a file + const shell = ShellTool.from(Shell.name(Shell.acceptable())) + + // Use the active shell tool to create a file const command = `echo 'snapshot race test content' > ${path.join(dir, "race-test.txt")}` - yield* llm.toolMatch((hit) => JSON.stringify(hit.body).includes("create the file"), "bash", { + yield* llm.toolMatch((hit) => JSON.stringify(hit.body).includes("create the file"), shell, { command, description: "create test file", }) - yield* llm.textMatch((hit) => JSON.stringify(hit.body).includes("bash"), "done") + yield* llm.textMatch((hit) => JSON.stringify(hit.body).includes(shell), "done") // Seed user message yield* prompt.prompt({ @@ -217,7 +220,7 @@ it.live("tool execution produces non-empty session diff (snapshot race)", () => const allMsgs = yield* MessageV2.filterCompactedEffect(session.id) const tool = allMsgs .flatMap((m) => m.parts) - .find((p): p is MessageV2.ToolPart => p.type === "tool" && p.tool === "bash") + .find((p): p is MessageV2.ToolPart => p.type === "tool" && p.tool === shell) expect(tool?.state.status).toBe("completed") // Poll for diff — summarize() is fire-and-forget diff --git a/packages/opencode/test/tool/shell.test.ts b/packages/opencode/test/tool/shell.test.ts index 224e4d6fc5..26a6e7d056 100644 --- a/packages/opencode/test/tool/shell.test.ts +++ b/packages/opencode/test/tool/shell.test.ts @@ -3,6 +3,7 @@ import os from "os" import path from "path" import { Shell } from "../../src/shell/shell" import { BashTool } from "../../src/tool/shell/bash" +import { ShellTool } from "../../src/tool/shell/id" import { PwshTool } from "../../src/tool/shell/pwsh" import { PowershellTool } from "../../src/tool/shell/powershell" import { Instance } from "../../src/project/instance" @@ -47,15 +48,14 @@ const shells = (() => { (item, i) => list.findIndex((other) => other.shell.toLowerCase() === item.shell.toLowerCase()) === i, ) })() -const PS = new Set(["pwsh", "powershell"]) -const ps = shells.filter((item) => PS.has(item.label)) +const ps = shells.filter((item) => ShellTool.powershell(item.label)) const sh = () => Shell.name(Shell.acceptable()) const evalarg = (text: string) => (sh() === "cmd" ? quote(text) : squote(text)) const js = (code: string, ...args: Array) => { const tail = args.length ? ` ${args.map(String).join(" ")}` : "" const text = `${bin} -e ${evalarg(code)}${tail}` - if (PS.has(sh())) return `& ${text}` + if (ShellTool.powershell(sh())) return `& ${text}` return text } @@ -92,18 +92,12 @@ const withShell = (item: { label: string; shell: string }, fn: () => Promise { - const name = sh() - if (name === "pwsh") return "pwsh" - if (name === "powershell") return "powershell" - return "bash" -} +const expectedPermission = () => ShellTool.from(sh()) + +const tools = { bash: BashTool, pwsh: PwshTool, powershell: PowershellTool } as const const getTool = async () => { - const name = sh() - if (name === "pwsh") return await PwshTool.init() - if (name === "powershell") return await PowershellTool.init() - return await BashTool.init() + return await tools[ShellTool.from(sh())].init() } const each = (name: string, fn: (item: { label: string; shell: string }) => Promise) => { diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx index a2e644d887..a1b1b00c9c 100644 --- a/packages/ui/src/components/message-part.tsx +++ b/packages/ui/src/components/message-part.tsx @@ -278,6 +278,14 @@ function agentTitle(i18n: UiI18n, type?: string) { export function getToolInfo(tool: string, input: any = {}): ToolInfo { const i18n = useI18n() + if (SHELL.has(tool)) { + return { + icon: "console", + title: i18n.t("ui.tool.shell"), + subtitle: input.description, + } + } + switch (tool) { case "read": return { @@ -332,14 +340,6 @@ export function getToolInfo(tool: string, input: any = {}): ToolInfo { subtitle: input.description, } } - case "bash": - case "pwsh": - case "powershell": - return { - icon: "console", - title: i18n.t("ui.tool.shell"), - subtitle: input.description, - } case "edit": return { icon: "code-lines",