Skip to content

Commit 3f45981

Browse files
authored
feat: refactor bash tool with shell-aware prompts for bash, pwsh+powershell, and cmd (#20039)
1 parent 1986a6e commit 3f45981

14 files changed

Lines changed: 506 additions & 177 deletions

File tree

packages/opencode/src/acp/agent.ts

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ import { LoadAPIKeyError } from "ai"
5151
import type { AssistantMessage, Event, OpencodeClient, SessionMessageResponse, ToolPart } from "@opencode-ai/sdk/v2"
5252
import { applyPatch } from "diff"
5353
import { InstallationVersion } from "@opencode-ai/core/installation/version"
54+
import { ShellID } from "@/tool/shell/id"
5455

5556
type ModeOption = { id: string; name: string; description?: string }
5657
type ModelOption = { modelId: string; name: string }
@@ -144,7 +145,7 @@ export class Agent implements ACPAgent {
144145
private sessionManager: ACPSessionManager
145146
private eventAbort = new AbortController()
146147
private eventStarted = false
147-
private bashSnapshots = new Map<string, string>()
148+
private shellSnapshots = new Map<string, string>()
148149
private toolStarts = new Set<string>()
149150
private permissionQueues = new Map<string, Promise<void>>()
150151
private permissionOptions: PermissionOption[] = [
@@ -283,16 +284,16 @@ export class Agent implements ACPAgent {
283284

284285
switch (part.state.status) {
285286
case "pending":
286-
this.bashSnapshots.delete(part.callID)
287+
this.shellSnapshots.delete(part.callID)
287288
return
288289

289290
case "running":
290-
const output = this.bashOutput(part)
291+
const output = this.shellOutput(part)
291292
const content: ToolCallContent[] = []
292293
if (output) {
293294
const hash = Hash.fast(output)
294-
if (part.tool === "bash") {
295-
if (this.bashSnapshots.get(part.callID) === hash) {
295+
if (part.tool === ShellID.ToolID) {
296+
if (this.shellSnapshots.get(part.callID) === hash) {
296297
await this.connection
297298
.sessionUpdate({
298299
sessionId,
@@ -311,7 +312,7 @@ export class Agent implements ACPAgent {
311312
})
312313
return
313314
}
314-
this.bashSnapshots.set(part.callID, hash)
315+
this.shellSnapshots.set(part.callID, hash)
315316
}
316317
content.push({
317318
type: "content",
@@ -342,7 +343,7 @@ export class Agent implements ACPAgent {
342343

343344
case "completed": {
344345
this.toolStarts.delete(part.callID)
345-
this.bashSnapshots.delete(part.callID)
346+
this.shellSnapshots.delete(part.callID)
346347
const kind = toToolKind(part.tool)
347348
const content: ToolCallContent[] = [
348349
{
@@ -423,7 +424,7 @@ export class Agent implements ACPAgent {
423424
}
424425
case "error":
425426
this.toolStarts.delete(part.callID)
426-
this.bashSnapshots.delete(part.callID)
427+
this.shellSnapshots.delete(part.callID)
427428
await this.connection
428429
.sessionUpdate({
429430
sessionId,
@@ -837,10 +838,10 @@ export class Agent implements ACPAgent {
837838
await this.toolStart(sessionId, part)
838839
switch (part.state.status) {
839840
case "pending":
840-
this.bashSnapshots.delete(part.callID)
841+
this.shellSnapshots.delete(part.callID)
841842
break
842843
case "running":
843-
const output = this.bashOutput(part)
844+
const output = this.shellOutput(part)
844845
const runningContent: ToolCallContent[] = []
845846
if (output) {
846847
runningContent.push({
@@ -871,7 +872,7 @@ export class Agent implements ACPAgent {
871872
break
872873
case "completed":
873874
this.toolStarts.delete(part.callID)
874-
this.bashSnapshots.delete(part.callID)
875+
this.shellSnapshots.delete(part.callID)
875876
const kind = toToolKind(part.tool)
876877
const content: ToolCallContent[] = [
877878
{
@@ -951,7 +952,7 @@ export class Agent implements ACPAgent {
951952
break
952953
case "error":
953954
this.toolStarts.delete(part.callID)
954-
this.bashSnapshots.delete(part.callID)
955+
this.shellSnapshots.delete(part.callID)
955956
await this.connection
956957
.sessionUpdate({
957958
sessionId,
@@ -1105,8 +1106,8 @@ export class Agent implements ACPAgent {
11051106
}
11061107
}
11071108

1108-
private bashOutput(part: ToolPart) {
1109-
if (part.tool !== "bash") return
1109+
private shellOutput(part: ToolPart) {
1110+
if (part.tool !== ShellID.ToolID) return
11101111
if (!("metadata" in part.state) || !part.state.metadata || typeof part.state.metadata !== "object") return
11111112
const output = part.state.metadata["output"]
11121113
if (typeof output !== "string") return
@@ -1549,9 +1550,11 @@ export class Agent implements ACPAgent {
15491550

15501551
function toToolKind(toolName: string): ToolKind {
15511552
const tool = toolName.toLocaleLowerCase()
1553+
15521554
switch (tool) {
1553-
case "bash":
1555+
case ShellID.ToolID:
15541556
return "execute"
1557+
15551558
case "webfetch":
15561559
return "fetch"
15571560

@@ -1576,6 +1579,7 @@ function toToolKind(toolName: string): ToolKind {
15761579

15771580
function toLocations(toolName: string, input: Record<string, any>): { path: string }[] {
15781581
const tool = toolName.toLocaleLowerCase()
1582+
15791583
switch (tool) {
15801584
case "read":
15811585
case "edit":
@@ -1584,7 +1588,7 @@ function toLocations(toolName: string, input: Record<string, any>): { path: stri
15841588
case "glob":
15851589
case "grep":
15861590
return input["path"] ? [{ path: input["path"] }] : []
1587-
case "bash":
1591+
case ShellID.ToolID:
15881592
return []
15891593
default:
15901594
return []

packages/opencode/src/cli/cmd/github.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -879,7 +879,7 @@ export const GithubRunCommand = cmd({
879879
function subscribeSessionEvents() {
880880
const TOOL: Record<string, [string, string]> = {
881881
todowrite: ["Todo", UI.Style.TEXT_WARNING_BOLD],
882-
bash: ["Bash", UI.Style.TEXT_DANGER_BOLD],
882+
bash: ["Shell", UI.Style.TEXT_DANGER_BOLD],
883883
edit: ["Edit", UI.Style.TEXT_SUCCESS_BOLD],
884884
glob: ["Glob", UI.Style.TEXT_INFO_BOLD],
885885
grep: ["Grep", UI.Style.TEXT_INFO_BOLD],

packages/opencode/src/cli/cmd/run.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ import { WriteTool } from "../../tool/write"
2222
import { WebSearchTool } from "../../tool/websearch"
2323
import { TaskTool } from "../../tool/task"
2424
import { SkillTool } from "../../tool/skill"
25-
import { BashTool } from "../../tool/bash"
25+
import { ShellTool } from "../../tool/shell"
26+
import { ShellID } from "../../tool/shell/id"
2627
import { TodoWriteTool } from "../../tool/todo"
2728
import { Locale } from "@/util/locale"
2829
import { AppRuntime } from "@/effect/app-runtime"
@@ -175,7 +176,7 @@ function skill(info: ToolProps<typeof SkillTool>) {
175176
})
176177
}
177178

178-
function bash(info: ToolProps<typeof BashTool>) {
179+
function shell(info: ToolProps<typeof ShellTool>) {
179180
const output = info.part.state.status === "completed" ? info.part.state.output?.trim() : undefined
180181
block(
181182
{
@@ -400,7 +401,7 @@ export const RunCommand = cmd({
400401
async function execute(sdk: OpencodeClient) {
401402
function tool(part: ToolPart) {
402403
try {
403-
if (part.tool === "bash") return bash(props<typeof BashTool>(part))
404+
if (part.tool === ShellID.ToolID) return shell(props<typeof ShellTool>(part))
404405
if (part.tool === "glob") return glob(props<typeof GlobTool>(part))
405406
if (part.tool === "grep") return grep(props<typeof GrepTool>(part))
406407
if (part.tool === "read") return read(props<typeof ReadTool>(part))

packages/opencode/src/cli/cmd/tui/routes/session/index.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ import { Locale } from "@/util/locale"
3737
import type { Tool } from "@/tool/tool"
3838
import type { ReadTool } from "@/tool/read"
3939
import type { WriteTool } from "@/tool/write"
40-
import { BashTool } from "@/tool/bash"
40+
import { ShellTool } from "@/tool/shell"
41+
import { ShellID } from "@/tool/shell/id"
4142
import type { GlobTool } from "@/tool/glob"
4243
import { TodoWriteTool } from "@/tool/todo"
4344
import type { GrepTool } from "@/tool/grep"
@@ -1552,8 +1553,8 @@ function ToolPart(props: { last: boolean; part: ToolPart; message: AssistantMess
15521553
return (
15531554
<Show when={!shouldHide()}>
15541555
<Switch>
1555-
<Match when={props.part.tool === "bash"}>
1556-
<Bash {...toolprops} />
1556+
<Match when={props.part.tool === ShellID.ToolID}>
1557+
<Shell {...toolprops} />
15571558
</Match>
15581559
<Match when={props.part.tool === "glob"}>
15591560
<Glob {...toolprops} />
@@ -1784,7 +1785,7 @@ function BlockTool(props: {
17841785
)
17851786
}
17861787

1787-
function Bash(props: ToolProps<typeof BashTool>) {
1788+
function Shell(props: ToolProps<typeof ShellTool>) {
17881789
const { theme } = useTheme()
17891790
const sync = useSync()
17901791
const isRunning = createMemo(() => props.part.state.status === "running")

packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { LANGUAGE_EXTENSIONS } from "@/lsp/language"
1515
import { Keybind } from "@/util/keybind"
1616
import { Locale } from "@/util/locale"
1717
import { Global } from "@opencode-ai/core/global"
18+
import { ShellID } from "@/tool/shell/id"
1819
import { useDialog } from "../../ui/dialog"
1920
import { getScrollAcceleration } from "../../util/scroll"
2021
import { useTuiConfig } from "../../context/tui-config"
@@ -287,7 +288,7 @@ export function PermissionPrompt(props: { request: PermissionRequest }) {
287288
}
288289
}
289290

290-
if (permission === "bash") {
291+
if (permission === ShellID.ToolID) {
291292
const title =
292293
typeof data.description === "string" && data.description ? data.description : "Shell command"
293294
const command = typeof data.command === "string" ? data.command : ""

packages/opencode/src/session/prompt.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import { Permission } from "@/permission"
4141
import { SessionStatus } from "./status"
4242
import { LLM } from "./llm"
4343
import { Shell } from "@/shell/shell"
44+
import { ShellID } from "@/tool/shell/id"
4445
import { AppFileSystem } from "@opencode-ai/core/filesystem"
4546
import { Truncate } from "@/tool/truncate"
4647
import { decodeDataUrl } from "@/util/data-url"
@@ -789,7 +790,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
789790
id: PartID.ascending(),
790791
messageID: msg.id,
791792
sessionID: input.sessionID,
792-
tool: "bash",
793+
tool: ShellID.ToolID,
793794
callID: ulid(),
794795
state: {
795796
status: "running",

packages/opencode/src/tool/registry.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { PlanExitTool } from "./plan"
22
import { Session } from "@/session/session"
33
import { QuestionTool } from "./question"
4-
import { BashTool } from "./bash"
4+
import { ShellTool } from "./shell"
55
import { EditTool } from "./edit"
66
import { GlobTool } from "./glob"
77
import { GrepTool } from "./grep"
@@ -106,7 +106,7 @@ export const layer: Layer.Layer<
106106
const plan = yield* PlanExitTool
107107
const webfetch = yield* WebFetchTool
108108
const websearch = yield* WebSearchTool
109-
const bash = yield* BashTool
109+
const shell = yield* ShellTool
110110
const globtool = yield* GlobTool
111111
const writetool = yield* WriteTool
112112
const edit = yield* EditTool
@@ -195,7 +195,7 @@ export const layer: Layer.Layer<
195195

196196
const tool = yield* Effect.all({
197197
invalid: Tool.init(invalid),
198-
bash: Tool.init(bash),
198+
shell: Tool.init(shell),
199199
read: Tool.init(read),
200200
glob: Tool.init(globtool),
201201
grep: Tool.init(greptool),
@@ -217,7 +217,7 @@ export const layer: Layer.Layer<
217217
builtin: [
218218
tool.invalid,
219219
...(questionEnabled ? [tool.question] : []),
220-
tool.bash,
220+
tool.shell,
221221
tool.read,
222222
tool.glob,
223223
tool.grep,

0 commit comments

Comments
 (0)