feat: refactor bash tool with shell-aware prompts for bash, pwsh+powershell, and cmd (#20039)

This commit is contained in:
Luke Parker
2026-05-03 09:18:48 +10:00
committed by GitHub
parent 1986a6e817
commit 3f459819ba
14 changed files with 506 additions and 177 deletions

View File

@@ -51,6 +51,7 @@ import { LoadAPIKeyError } from "ai"
import type { AssistantMessage, Event, OpencodeClient, SessionMessageResponse, ToolPart } from "@opencode-ai/sdk/v2"
import { applyPatch } from "diff"
import { InstallationVersion } from "@opencode-ai/core/installation/version"
import { ShellID } from "@/tool/shell/id"
type ModeOption = { id: string; name: string; description?: string }
type ModelOption = { modelId: string; name: string }
@@ -144,7 +145,7 @@ export class Agent implements ACPAgent {
private sessionManager: ACPSessionManager
private eventAbort = new AbortController()
private eventStarted = false
private bashSnapshots = new Map<string, string>()
private shellSnapshots = new Map<string, string>()
private toolStarts = new Set<string>()
private permissionQueues = new Map<string, Promise<void>>()
private permissionOptions: PermissionOption[] = [
@@ -283,16 +284,16 @@ export class Agent implements ACPAgent {
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") {
if (this.bashSnapshots.get(part.callID) === hash) {
if (part.tool === ShellID.ToolID) {
if (this.shellSnapshots.get(part.callID) === hash) {
await this.connection
.sessionUpdate({
sessionId,
@@ -311,7 +312,7 @@ export class Agent implements ACPAgent {
})
return
}
this.bashSnapshots.set(part.callID, hash)
this.shellSnapshots.set(part.callID, hash)
}
content.push({
type: "content",
@@ -342,7 +343,7 @@ export class Agent implements ACPAgent {
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[] = [
{
@@ -423,7 +424,7 @@ export class Agent implements ACPAgent {
}
case "error":
this.toolStarts.delete(part.callID)
this.bashSnapshots.delete(part.callID)
this.shellSnapshots.delete(part.callID)
await this.connection
.sessionUpdate({
sessionId,
@@ -837,10 +838,10 @@ export class Agent implements ACPAgent {
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({
@@ -871,7 +872,7 @@ export class Agent implements ACPAgent {
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[] = [
{
@@ -951,7 +952,7 @@ export class Agent implements ACPAgent {
break
case "error":
this.toolStarts.delete(part.callID)
this.bashSnapshots.delete(part.callID)
this.shellSnapshots.delete(part.callID)
await this.connection
.sessionUpdate({
sessionId,
@@ -1105,8 +1106,8 @@ export class Agent implements ACPAgent {
}
}
private bashOutput(part: ToolPart) {
if (part.tool !== "bash") return
private shellOutput(part: ToolPart) {
if (part.tool !== ShellID.ToolID) 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
@@ -1549,9 +1550,11 @@ export class Agent implements ACPAgent {
function toToolKind(toolName: string): ToolKind {
const tool = toolName.toLocaleLowerCase()
switch (tool) {
case "bash":
case ShellID.ToolID:
return "execute"
case "webfetch":
return "fetch"
@@ -1576,6 +1579,7 @@ function toToolKind(toolName: string): ToolKind {
function toLocations(toolName: string, input: Record<string, any>): { path: string }[] {
const tool = toolName.toLocaleLowerCase()
switch (tool) {
case "read":
case "edit":
@@ -1584,7 +1588,7 @@ function toLocations(toolName: string, input: Record<string, any>): { path: stri
case "glob":
case "grep":
return input["path"] ? [{ path: input["path"] }] : []
case "bash":
case ShellID.ToolID:
return []
default:
return []