fix(cli): rename bash tool to shell

This commit is contained in:
Dax Raad 2026-07-14 21:31:42 -04:00
commit 05fdbcce04
4 changed files with 38 additions and 20 deletions

View file

@ -9,7 +9,7 @@
// Slash commands: // Slash commands:
// /permission [kind] → triggers a permission request variant // /permission [kind] → triggers a permission request variant
// /question [kind] → triggers a question request variant // /question [kind] → triggers a question request variant
// /fmt <kind> → emits a specific tool/text type (text, reasoning, bash, // /fmt <kind> → emits a specific tool/text type (text, reasoning, shell,
// write, edit, patch, task, question, error, mix) // write, edit, patch, task, question, error, mix)
// //
// Demo mode also handles permission and question replies locally, completing // Demo mode also handles permission and question replies locally, completing
@ -33,7 +33,7 @@ const KINDS = [
"table", "table",
"text", "text",
"reasoning", "reasoning",
"bash", "shell",
"write", "write",
"edit", "edit",
"patch", "patch",
@ -42,7 +42,7 @@ const KINDS = [
"error", "error",
"mix", "mix",
] ]
const PERMISSIONS = ["edit", "bash", "read", "task", "external", "doom"] as const const PERMISSIONS = ["edit", "shell", "read", "task", "external", "doom"] as const
const QUESTIONS = ["multi", "single", "checklist", "custom"] as const const QUESTIONS = ["multi", "single", "checklist", "custom"] as const
type PermissionKind = (typeof PERMISSIONS)[number] type PermissionKind = (typeof PERMISSIONS)[number]
@ -436,7 +436,7 @@ function emitError(state: State, text: string): void {
} }
async function emitBash(state: State, signal?: AbortSignal): Promise<void> { async function emitBash(state: State, signal?: AbortSignal): Promise<void> {
const ref = make(state, "bash", { const ref = make(state, "shell", {
command: "git status", command: "git status",
workdir: process.cwd(), workdir: process.cwd(),
description: "Show git status", description: "Show git status",
@ -623,16 +623,16 @@ function emitPermission(state: State, kind: PermissionKind = "edit"): void {
const root = process.cwd() const root = process.cwd()
const file = path.join(root, "src", "demo-format.ts") const file = path.join(root, "src", "demo-format.ts")
if (kind === "bash") { if (kind === "shell") {
const command = "git status --short" const command = "git status --short"
const ref = make(state, "bash", { const ref = make(state, "shell", {
command, command,
workdir: root, workdir: root,
description: "Inspect worktree changes", description: "Inspect worktree changes",
}) })
askPermission(state, { askPermission(state, {
ref, ref,
permission: "bash", permission: "shell",
patterns: [command], patterns: [command],
always: ["*"], always: ["*"],
done: { done: {
@ -862,7 +862,7 @@ async function emitFmt(state: State, kind: string, body: string, signal?: AbortS
return true return true
} }
if (kind === "bash") { if (kind === "shell") {
await emitBash(state, signal) await emitBash(state, signal)
return true return true
} }
@ -924,7 +924,7 @@ function intro(state: State): void {
`- /question [kind] (${QUESTIONS.join(", ")})`, `- /question [kind] (${QUESTIONS.join(", ")})`,
`- /fmt <kind> (${KINDS.join(", ")})`, `- /fmt <kind> (${KINDS.join(", ")})`,
"Examples:", "Examples:",
"- /permission bash", "- /permission shell",
"- /question custom", "- /question custom",
"- /fmt markdown", "- /fmt markdown",
"- /fmt table", "- /fmt table",

View file

@ -218,7 +218,7 @@ function shellCommit(
kind: "tool", kind: "tool",
source: "tool", source: "tool",
partID: `shell:${callID}`, partID: `shell:${callID}`,
tool: "bash", tool: "shell",
shell: { callID, command }, shell: { callID, command },
...next, ...next,
} }

View file

@ -1,6 +1,6 @@
// Per-tool display rules shared across `opencode run` output paths. // Per-tool display rules shared across `opencode run` output paths.
// //
// Each known tool (bash, edit, write, task, etc.) has a ToolRule that controls // Each known tool (shell, edit, write, task, etc.) has a ToolRule that controls
// five display hooks: // five display hooks:
// //
// view → visibility policy for progress/final scrollback entries and // view → visibility policy for progress/final scrollback entries and
@ -114,7 +114,7 @@ type ToolPermissionCtx = {
type ToolName = type ToolName =
| "invalid" | "invalid"
| "bash" | "shell"
| "write" | "write"
| "edit" | "edit"
| "patch" | "patch"
@ -648,7 +648,7 @@ function scrollBashProgress(p: ToolProps): string {
return fmt(out) return fmt(out)
} }
function scrollBashFinal(p: ToolProps): string { function scrollShellFinal(p: ToolProps): string {
if (p.frame.status === "error") { if (p.frame.status === "error") {
return fail(p.frame) return fail(p.frame)
} }
@ -657,13 +657,13 @@ function scrollBashFinal(p: ToolProps): string {
const time = span(p.frame.state) const time = span(p.frame.state)
if (code === undefined) { if (code === undefined) {
if (!time) { if (!time) {
return "bash completed" return "shell completed"
} }
return `bash completed · ${time}` return `shell completed · ${time}`
} }
return `bash completed (exit ${code})${time ? ` · ${time}` : ""}` return `shell completed (exit ${code})${time ? ` · ${time}` : ""}`
} }
function scrollReadStart(p: ToolProps): string { function scrollReadStart(p: ToolProps): string {
@ -976,16 +976,16 @@ const TOOL_RULES = {
start: () => "", start: () => "",
}, },
}, },
bash: { shell: {
view: { view: {
output: true, output: true,
final: false, final: false,
}, },
run: runBash, run: runShell,
scroll: { scroll: {
start: scrollBashStart, start: scrollBashStart,
progress: scrollBashProgress, progress: scrollBashProgress,
final: scrollBashFinal, final: scrollShellFinal,
}, },
permission: permBash, permission: permBash,
}, },
@ -1202,7 +1202,7 @@ export function toolFrame(commit: StreamCommit, raw: string): ToolFrame {
} }
} }
function runBash(p: ToolProps): ToolInline { function runShell(p: ToolProps): ToolInline {
return { return {
icon: "$", icon: "$",
title: p.input.command || "", title: p.input.command || "",

View file

@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test"
import { InstallationVersion } from "@opencode-ai/core/installation/version" import { InstallationVersion } from "@opencode-ai/core/installation/version"
import path from "node:path" import path from "node:path"
import { mergeInteractiveInput, mergeNonInteractiveInput, parseRunModel, pickRunModel } from "../src/mini" import { mergeInteractiveInput, mergeNonInteractiveInput, parseRunModel, pickRunModel } from "../src/mini"
import { toolInlineInfo, toolView } from "../src/mini/tool"
async function cli(args: string[]) { async function cli(args: string[]) {
const child = Bun.spawn([process.execPath, "run", "src/index.ts", ...args], { const child = Bun.spawn([process.execPath, "run", "src/index.ts", ...args], {
@ -18,6 +19,23 @@ async function cli(args: string[]) {
} }
describe("mini command", () => { describe("mini command", () => {
test("renders the renamed shell tool with the shell rule", () => {
const part = {
id: "part-shell",
sessionID: "session-shell",
messageID: "message-shell",
callID: "call-shell",
tool: "shell",
state: {
status: "pending" as const,
input: { command: "pwd" },
},
} as const
expect(toolView(part.tool)).toEqual({ output: true, final: false })
expect(toolInlineInfo(part)).toMatchObject({ icon: "$", title: "pwd", mode: "block" })
})
test("uses piped stdin as the initial prompt", () => { test("uses piped stdin as the initial prompt", () => {
expect(mergeInteractiveInput("from stdin", undefined)).toBe("from stdin") expect(mergeInteractiveInput("from stdin", undefined)).toBe("from stdin")
expect(mergeInteractiveInput("from stdin", "from flag")).toBe("from stdin\nfrom flag") expect(mergeInteractiveInput("from stdin", "from flag")).toBe("from stdin\nfrom flag")