mirror of
https://github.com/anomalyco/opencode.git
synced 2026-05-07 00:51:34 +00:00
ok
This commit is contained in:
parent
39088e1a1e
commit
f1547de528
57 changed files with 310 additions and 294 deletions
|
|
@ -401,7 +401,7 @@ test("blocked permission flow supports allow once", async ({ page, project }) =>
|
|||
{
|
||||
id: "per_e2e_once",
|
||||
sessionID: session.id,
|
||||
permission: "bash",
|
||||
permission: "shell",
|
||||
patterns: ["/tmp/opencode-e2e-perm-once"],
|
||||
metadata: { description: "Need permission for command" },
|
||||
},
|
||||
|
|
@ -434,7 +434,7 @@ test("blocked permission flow supports reject", async ({ page, project }) => {
|
|||
{
|
||||
id: "per_e2e_reject",
|
||||
sessionID: session.id,
|
||||
permission: "bash",
|
||||
permission: "shell",
|
||||
patterns: ["/tmp/opencode-e2e-perm-reject"],
|
||||
},
|
||||
undefined,
|
||||
|
|
@ -466,7 +466,7 @@ test("blocked permission flow supports allow always", async ({ page, project })
|
|||
{
|
||||
id: "per_e2e_always",
|
||||
sessionID: session.id,
|
||||
permission: "bash",
|
||||
permission: "shell",
|
||||
patterns: ["/tmp/opencode-e2e-perm-always"],
|
||||
metadata: { description: "Need permission for command" },
|
||||
},
|
||||
|
|
@ -561,7 +561,7 @@ test("child session permission request blocks parent dock and supports allow onc
|
|||
{
|
||||
id: "per_e2e_child",
|
||||
sessionID: child.id,
|
||||
permission: "bash",
|
||||
permission: "shell",
|
||||
patterns: ["/tmp/opencode-e2e-perm-child"],
|
||||
metadata: { description: "Need child permission" },
|
||||
},
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ 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, "\\$&")
|
||||
|
||||
|
|
@ -34,7 +33,7 @@ export function createSdk(directory?: string, baseUrl = serverUrl) {
|
|||
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
|
||||
if (!("tool" in part) || part.tool !== "shell") return false
|
||||
return "state" in part
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,8 +14,9 @@ export function SessionPermissionDock(props: {
|
|||
|
||||
const toolDescription = () => {
|
||||
const key = `settings.permissions.tool.${props.request.permission}.description`
|
||||
const fallback = props.request.permission === "shell" ? "settings.permissions.tool.bash.description" : key
|
||||
const value = language.t(key as Parameters<typeof language.t>[0])
|
||||
if (value === key) return ""
|
||||
if (value === key) return fallback === key ? "" : language.t(fallback as Parameters<typeof language.t>[0])
|
||||
return value
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -41,7 +41,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 { ShellToolID } from "@/tool/shell/id"
|
||||
import { MessageV2 } from "@/session/message-v2"
|
||||
import { Config } from "@/config/config"
|
||||
import { Todo } from "@/session/todo"
|
||||
|
|
@ -289,7 +289,7 @@ export namespace ACP {
|
|||
const content: ToolCallContent[] = []
|
||||
if (output) {
|
||||
const hash = Hash.fast(output)
|
||||
if (ShellTool.has(part.tool)) {
|
||||
if (ShellToolID.has(part.tool)) {
|
||||
if (this.shellSnapshots.get(part.callID) === hash) {
|
||||
await this.connection
|
||||
.sessionUpdate({
|
||||
|
|
@ -1111,7 +1111,7 @@ export namespace ACP {
|
|||
}
|
||||
|
||||
private shellOutput(part: ToolPart) {
|
||||
if (!ShellTool.has(part.tool)) return
|
||||
if (!ShellToolID.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
|
||||
|
|
@ -1555,7 +1555,7 @@ export namespace ACP {
|
|||
|
||||
function toToolKind(toolName: string): ToolKind {
|
||||
const tool = toolName.toLocaleLowerCase()
|
||||
if (ShellTool.has(tool)) return "execute"
|
||||
if (ShellToolID.has(tool)) return "execute"
|
||||
|
||||
switch (tool) {
|
||||
case "webfetch":
|
||||
|
|
@ -1583,7 +1583,7 @@ export namespace ACP {
|
|||
|
||||
function toLocations(toolName: string, input: Record<string, any>): { path: string }[] {
|
||||
const tool = toolName.toLocaleLowerCase()
|
||||
if (ShellTool.has(tool)) return []
|
||||
if (ShellToolID.has(tool)) return []
|
||||
|
||||
switch (tool) {
|
||||
case "read":
|
||||
|
|
|
|||
|
|
@ -167,7 +167,7 @@ export namespace Agent {
|
|||
grep: "allow",
|
||||
glob: "allow",
|
||||
list: "allow",
|
||||
bash: "allow",
|
||||
shell: "allow",
|
||||
webfetch: "allow",
|
||||
websearch: "allow",
|
||||
codesearch: "allow",
|
||||
|
|
|
|||
|
|
@ -9,10 +9,10 @@ Guidelines:
|
|||
- Use Glob for broad file pattern matching
|
||||
- Use Grep for searching file contents with regex
|
||||
- Use Read when you know the specific file path you need to read
|
||||
- Use Bash for file operations like copying, moving, or listing directory contents
|
||||
- Use Shell for file operations like copying, moving, or listing directory contents
|
||||
- Adapt your search approach based on the thoroughness level specified by the caller
|
||||
- Return file paths as absolute paths in your final response
|
||||
- For clear communication, avoid using emojis
|
||||
- Do not create any files, or run bash commands that modify the user's system state in any way
|
||||
- Do not create any files, or run shell commands that modify the user's system state in any way
|
||||
|
||||
Complete the user's search request efficiently and report your findings clearly.
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ Your output must be:
|
|||
<rules>
|
||||
- you MUST use the same language as the user message you are summarizing
|
||||
- Title must be grammatically correct and read naturally - no word salad
|
||||
- Never include tool names in the title (e.g. "read tool", "bash tool", "edit tool")
|
||||
- Never include tool names in the title (e.g. "read tool", "shell tool", "edit tool")
|
||||
- Focus on the main topic or question the user needs to retrieve
|
||||
- Vary your phrasing - avoid repetitive patterns like always starting with "Analyzing"
|
||||
- When a file is mentioned, focus on WHAT the user wants to do WITH the file, not just that they shared it
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import type { Argv } from "yargs"
|
|||
|
||||
type AgentMode = "all" | "primary" | "subagent"
|
||||
|
||||
const AVAILABLE_TOOLS = ["bash", "read", "write", "edit", "list", "glob", "grep", "webfetch", "task", "todowrite"]
|
||||
const AVAILABLE_TOOLS = ["shell", "read", "write", "edit", "list", "glob", "grep", "webfetch", "task", "todowrite"]
|
||||
|
||||
const AgentCreateCommand = cmd({
|
||||
command: "create",
|
||||
|
|
|
|||
|
|
@ -869,7 +869,8 @@ export const GithubRunCommand = cmd({
|
|||
function subscribeSessionEvents() {
|
||||
const TOOL: Record<string, [string, string]> = {
|
||||
todowrite: ["Todo", UI.Style.TEXT_WARNING_BOLD],
|
||||
bash: ["Bash", UI.Style.TEXT_DANGER_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],
|
||||
|
|
|
|||
|
|
@ -24,8 +24,8 @@ import { CodeSearchTool } from "../../tool/codesearch"
|
|||
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 { ShellTool } from "../../tool/shell/tool"
|
||||
import { ShellToolID } from "../../tool/shell/id"
|
||||
import { TodoWriteTool } from "../../tool/todo"
|
||||
import { Locale } from "../../util/locale"
|
||||
|
||||
|
|
@ -192,7 +192,7 @@ function skill(info: ToolProps<typeof SkillTool>) {
|
|||
})
|
||||
}
|
||||
|
||||
function bash(info: ToolProps<typeof BashTool>) {
|
||||
function shell(info: ToolProps<typeof ShellTool>) {
|
||||
const output = info.part.state.status === "completed" ? info.part.state.output?.trim() : undefined
|
||||
block(
|
||||
{
|
||||
|
|
@ -417,7 +417,7 @@ export const RunCommand = cmd({
|
|||
async function execute(sdk: OpencodeClient) {
|
||||
function tool(part: ToolPart) {
|
||||
try {
|
||||
if (ShellTool.has(part.tool)) return bash(props<typeof BashTool>(part))
|
||||
if (ShellToolID.has(part.tool)) return shell(props<typeof ShellTool>(part))
|
||||
if (part.tool === "glob") return glob(props<typeof GlobTool>(part))
|
||||
if (part.tool === "grep") return grep(props<typeof GrepTool>(part))
|
||||
if (part.tool === "list") return list(props<typeof ListTool>(part))
|
||||
|
|
|
|||
|
|
@ -92,8 +92,8 @@ const TIPS = [
|
|||
"Use {highlight}$ARGUMENTS{/highlight}, {highlight}$1{/highlight}, {highlight}$2{/highlight} in custom commands for dynamic input",
|
||||
"Use backticks in commands to inject shell output (e.g., {highlight}`git status`{/highlight})",
|
||||
"Add {highlight}.md{/highlight} files to {highlight}.opencode/agent/{/highlight} for specialized AI personas",
|
||||
"Configure per-agent permissions for {highlight}edit{/highlight}, {highlight}bash{/highlight}, and {highlight}webfetch{/highlight} tools",
|
||||
'Use patterns like {highlight}"git *": "allow"{/highlight} for granular bash permissions',
|
||||
"Configure per-agent permissions for {highlight}edit{/highlight}, {highlight}shell{/highlight}, and {highlight}webfetch{/highlight} tools",
|
||||
'Use patterns like {highlight}"git *": "allow"{/highlight} for granular shell permissions',
|
||||
'Set {highlight}"rm -rf *": "deny"{/highlight} to block destructive commands',
|
||||
'Configure {highlight}"git push": "ask"{/highlight} to require approval before pushing',
|
||||
"OpenCode auto-formats files using prettier, gofmt, ruff, and more",
|
||||
|
|
@ -127,7 +127,7 @@ const TIPS = [
|
|||
"Use {highlight}instructions{/highlight} in config to load additional rules files",
|
||||
"Set agent {highlight}temperature{/highlight} from 0.0 (focused) to 1.0 (creative)",
|
||||
"Configure {highlight}steps{/highlight} to limit agentic iterations per request",
|
||||
'Set {highlight}"tools": {"bash": false}{/highlight} to disable specific tools',
|
||||
'Set {highlight}"tools": {"shell": false}{/highlight} to disable specific tools',
|
||||
'Set {highlight}"mcp_*": false{/highlight} to disable all tools from an MCP server',
|
||||
"Override global tool settings per agent configuration",
|
||||
'Set {highlight}"share": "auto"{/highlight} to automatically share all sessions',
|
||||
|
|
|
|||
|
|
@ -35,8 +35,8 @@ import { Locale } from "@/util/locale"
|
|||
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 { ShellTool } from "@/tool/shell/tool"
|
||||
import { ShellToolID } from "@/tool/shell/id"
|
||||
import type { GlobTool } from "@/tool/glob"
|
||||
import { TodoWriteTool } from "@/tool/todo"
|
||||
import type { GrepTool } from "@/tool/grep"
|
||||
|
|
@ -1514,8 +1514,8 @@ function ToolPart(props: { last: boolean; part: ToolPart; message: AssistantMess
|
|||
return (
|
||||
<Show when={!shouldHide()}>
|
||||
<Switch>
|
||||
<Match when={ShellTool.has(props.part.tool)}>
|
||||
<Bash {...toolprops} />
|
||||
<Match when={ShellToolID.has(props.part.tool)}>
|
||||
<Shell {...toolprops} />
|
||||
</Match>
|
||||
<Match when={props.part.tool === "glob"}>
|
||||
<Glob {...toolprops} />
|
||||
|
|
@ -1752,7 +1752,7 @@ function BlockTool(props: {
|
|||
)
|
||||
}
|
||||
|
||||
function Bash(props: ToolProps<typeof BashTool>) {
|
||||
function Shell(props: ToolProps<typeof ShellTool>) {
|
||||
const { theme } = useTheme()
|
||||
const sync = useSync()
|
||||
const isRunning = createMemo(() => props.part.state.status === "running")
|
||||
|
|
|
|||
|
|
@ -14,7 +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 { ShellToolID } from "@/tool/shell/id"
|
||||
import { useDialog } from "../../ui/dialog"
|
||||
import { getScrollAcceleration } from "../../util/scroll"
|
||||
import { useTuiConfig } from "../../context/tui-config"
|
||||
|
|
@ -284,7 +284,7 @@ export function PermissionPrompt(props: { request: PermissionRequest }) {
|
|||
}
|
||||
}
|
||||
|
||||
if (ShellTool.has(permission)) {
|
||||
if (ShellToolID.has(permission)) {
|
||||
const title =
|
||||
typeof data.description === "string" && data.description ? data.description : "Shell command"
|
||||
const command = typeof data.command === "string" ? data.command : ""
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ import { AppFileSystem } from "@/filesystem"
|
|||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { makeRuntime } from "@/effect/run-service"
|
||||
import { Duration, Effect, Layer, Option, ServiceMap } from "effect"
|
||||
import { ShellToolID } from "@/tool/shell/id"
|
||||
import { Flock } from "@/util/flock"
|
||||
import { isPathPluginSpec, parsePluginSpecifier, resolvePathPluginTarget } from "@/plugin/shared"
|
||||
import { Npm } from "@/npm"
|
||||
|
|
@ -460,10 +461,15 @@ export namespace Config {
|
|||
if (typeof x === "string") return { "*": x as PermissionAction }
|
||||
const obj = x as { __originalKeys?: string[] } & Record<string, unknown>
|
||||
const { __originalKeys, ...rest } = obj
|
||||
if (!__originalKeys) return rest as Record<string, PermissionRule>
|
||||
if (!__originalKeys) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(rest).map(([key, value]) => [ShellToolID.normalize(key), value as PermissionRule]),
|
||||
)
|
||||
}
|
||||
const result: Record<string, PermissionRule> = {}
|
||||
for (const key of __originalKeys) {
|
||||
if (key in rest) result[key] = rest[key] as PermissionRule
|
||||
if (!(key in rest)) continue
|
||||
result[ShellToolID.normalize(key)] = rest[key] as PermissionRule
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
|
@ -479,7 +485,7 @@ export namespace Config {
|
|||
glob: PermissionRule.optional(),
|
||||
grep: PermissionRule.optional(),
|
||||
list: PermissionRule.optional(),
|
||||
bash: PermissionRule.optional(),
|
||||
shell: PermissionRule.optional(),
|
||||
task: PermissionRule.optional(),
|
||||
external_directory: PermissionRule.optional(),
|
||||
todowrite: PermissionAction.optional(),
|
||||
|
|
@ -587,8 +593,8 @@ export namespace Config {
|
|||
// write, edit, patch, multiedit all map to edit permission
|
||||
if (tool === "write" || tool === "edit" || tool === "patch" || tool === "multiedit") {
|
||||
permission.edit = action
|
||||
} else if (tool === "bash") {
|
||||
permission.bash = action
|
||||
} else if (ShellToolID.normalize(tool) === ShellToolID.id) {
|
||||
permission.shell = action
|
||||
} else {
|
||||
permission[tool] = action
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { Wildcard } from "@/util/wildcard"
|
||||
import { ShellToolID } from "@/tool/shell/id"
|
||||
|
||||
type Rule = {
|
||||
permission: string
|
||||
|
|
@ -7,9 +8,10 @@ type Rule = {
|
|||
}
|
||||
|
||||
export function evaluate(permission: string, pattern: string, ...rulesets: Rule[][]): Rule {
|
||||
const next = ShellToolID.normalize(permission)
|
||||
const rules = rulesets.flat()
|
||||
const match = rules.findLast(
|
||||
(rule) => Wildcard.match(permission, rule.permission) && Wildcard.match(pattern, rule.pattern),
|
||||
(rule) => Wildcard.match(next, ShellToolID.normalize(rule.permission)) && Wildcard.match(pattern, rule.pattern),
|
||||
)
|
||||
return match ?? { action: "ask", permission, pattern: "*" }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import os from "os"
|
|||
import z from "zod"
|
||||
import { evaluate as evalRule } from "./evaluate"
|
||||
import { PermissionID } from "./schema"
|
||||
import { ShellToolID } from "@/tool/shell/id"
|
||||
|
||||
export namespace Permission {
|
||||
const log = Log.create({ service: "permission" })
|
||||
|
|
@ -174,7 +175,9 @@ export namespace Permission {
|
|||
log.info("evaluated", { permission: request.permission, pattern, action: rule })
|
||||
if (rule.action === "deny") {
|
||||
return yield* new DeniedError({
|
||||
ruleset: ruleset.filter((rule) => Wildcard.match(request.permission, rule.permission)),
|
||||
ruleset: ruleset.filter((rule) =>
|
||||
Wildcard.match(ShellToolID.normalize(request.permission), ShellToolID.normalize(rule.permission)),
|
||||
),
|
||||
})
|
||||
}
|
||||
if (rule.action === "allow") continue
|
||||
|
|
@ -290,16 +293,8 @@ export namespace Permission {
|
|||
export function fromConfig(permission: Config.Permission) {
|
||||
const ruleset: Ruleset = []
|
||||
|
||||
const bash = permission["bash"]
|
||||
if (bash !== undefined) {
|
||||
pushRules(ruleset, "bash", bash)
|
||||
pushRules(ruleset, "pwsh", bash)
|
||||
pushRules(ruleset, "powershell", bash)
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(permission)) {
|
||||
if (key === "bash") continue
|
||||
pushRules(ruleset, key, value)
|
||||
pushRules(ruleset, ShellToolID.normalize(key), value)
|
||||
}
|
||||
return ruleset
|
||||
}
|
||||
|
|
@ -313,8 +308,8 @@ export namespace Permission {
|
|||
export function disabled(tools: string[], ruleset: Ruleset): Set<string> {
|
||||
const result = new Set<string>()
|
||||
for (const tool of tools) {
|
||||
const permission = EDIT_TOOLS.includes(tool) ? "edit" : tool
|
||||
const rule = ruleset.findLast((rule) => Wildcard.match(permission, rule.permission))
|
||||
const permission = EDIT_TOOLS.includes(tool) ? "edit" : ShellToolID.normalize(tool)
|
||||
const rule = ruleset.findLast((rule) => Wildcard.match(permission, ShellToolID.normalize(rule.permission)))
|
||||
if (!rule) continue
|
||||
if (rule.pattern === "*" && rule.action === "deny") result.add(tool)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -482,14 +482,15 @@ export namespace Session {
|
|||
|
||||
const updatePart = <T extends MessageV2.Part>(part: T): Effect.Effect<T> =>
|
||||
Effect.gen(function* () {
|
||||
const next = MessageV2.normalizePart(part)
|
||||
yield* Effect.sync(() =>
|
||||
SyncEvent.run(MessageV2.Event.PartUpdated, {
|
||||
sessionID: part.sessionID,
|
||||
part: structuredClone(part),
|
||||
sessionID: next.sessionID,
|
||||
part: structuredClone(next),
|
||||
time: Date.now(),
|
||||
}),
|
||||
)
|
||||
return part
|
||||
return next as T
|
||||
}).pipe(Effect.withSpan("Session.updatePart"))
|
||||
|
||||
const create = Effect.fn("Session.create")(function* (input?: {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import { Flag } from "@/flag/flag"
|
|||
import { Permission } from "@/permission"
|
||||
import { Auth } from "@/auth"
|
||||
import { Installation } from "@/installation"
|
||||
import { ShellToolID } from "@/tool/shell/id"
|
||||
|
||||
export namespace LLM {
|
||||
const log = Log.create({ service: "llm" })
|
||||
|
|
@ -226,6 +227,12 @@ export namespace LLM {
|
|||
})
|
||||
}
|
||||
|
||||
const repair = (toolName: string) => {
|
||||
const next = ShellToolID.normalize(toolName.toLowerCase())
|
||||
if (!tools[next]) return
|
||||
return next
|
||||
}
|
||||
|
||||
// Wire up toolExecutor for DWS workflow models so that tool calls
|
||||
// from the workflow service are executed via opencode's tool system
|
||||
// and results sent back over the WebSocket.
|
||||
|
|
@ -233,7 +240,7 @@ export namespace LLM {
|
|||
const workflowModel = language
|
||||
workflowModel.systemPrompt = system.join("\n")
|
||||
workflowModel.toolExecutor = async (toolName, argsJson, _requestID) => {
|
||||
const t = tools[toolName]
|
||||
const t = tools[repair(toolName) ?? toolName]
|
||||
if (!t || !t.execute) {
|
||||
return { result: "", error: `Unknown tool: ${toolName}` }
|
||||
}
|
||||
|
|
@ -262,15 +269,15 @@ export namespace LLM {
|
|||
})
|
||||
},
|
||||
async experimental_repairToolCall(failed) {
|
||||
const lower = failed.toolCall.toolName.toLowerCase()
|
||||
if (lower !== failed.toolCall.toolName && tools[lower]) {
|
||||
const repaired = repair(failed.toolCall.toolName)
|
||||
if (repaired && repaired !== failed.toolCall.toolName) {
|
||||
l.info("repairing tool call", {
|
||||
tool: failed.toolCall.toolName,
|
||||
repaired: lower,
|
||||
repaired,
|
||||
})
|
||||
return {
|
||||
...failed.toolCall,
|
||||
toolName: lower,
|
||||
toolName: repaired,
|
||||
}
|
||||
}
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import type { SystemError } from "bun"
|
|||
import type { Provider } from "@/provider/provider"
|
||||
import { ModelID, ProviderID } from "@/provider/schema"
|
||||
import { Effect } from "effect"
|
||||
import { ShellToolID } from "@/tool/shell/id"
|
||||
|
||||
/** Error shape thrown by Bun's fetch() when gzip/br decompression fails mid-stream */
|
||||
interface FetchDecompressionError extends Error {
|
||||
|
|
@ -24,6 +25,17 @@ interface FetchDecompressionError extends Error {
|
|||
}
|
||||
|
||||
export namespace MessageV2 {
|
||||
export function normalizeTool(tool: string) {
|
||||
return ShellToolID.normalize(tool)
|
||||
}
|
||||
|
||||
export function normalizePart<T extends Part>(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
|
||||
}
|
||||
|
||||
export function isMedia(mime: string) {
|
||||
return mime.startsWith("image/") || mime === "application/pdf"
|
||||
}
|
||||
|
|
@ -534,12 +546,12 @@ export namespace MessageV2 {
|
|||
}) as MessageV2.Info
|
||||
|
||||
const part = (row: typeof PartTable.$inferSelect) =>
|
||||
({
|
||||
normalizePart({
|
||||
...row.data,
|
||||
id: row.id,
|
||||
sessionID: row.session_id,
|
||||
messageID: row.message_id,
|
||||
}) as MessageV2.Part
|
||||
} as MessageV2.Part)
|
||||
|
||||
const older = (row: Cursor) =>
|
||||
or(
|
||||
|
|
@ -701,7 +713,8 @@ export namespace MessageV2 {
|
|||
role: "assistant",
|
||||
parts: [],
|
||||
}
|
||||
for (const part of msg.parts) {
|
||||
for (const raw of msg.parts) {
|
||||
const part = normalizePart(raw)
|
||||
if (part.type === "text")
|
||||
assistantMessage.parts.push({
|
||||
type: "text",
|
||||
|
|
@ -874,14 +887,13 @@ export namespace MessageV2 {
|
|||
const rows = Database.use((db) =>
|
||||
db.select().from(PartTable).where(eq(PartTable.message_id, message_id)).orderBy(PartTable.id).all(),
|
||||
)
|
||||
return rows.map(
|
||||
(row) =>
|
||||
({
|
||||
...row.data,
|
||||
id: row.id,
|
||||
sessionID: row.session_id,
|
||||
messageID: row.message_id,
|
||||
}) as MessageV2.Part,
|
||||
return rows.map((row) =>
|
||||
normalizePart({
|
||||
...row.data,
|
||||
id: row.id,
|
||||
sessionID: row.session_id,
|
||||
messageID: row.message_id,
|
||||
} as MessageV2.Part),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -40,7 +40,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 { ShellToolID } from "@/tool/shell/id"
|
||||
import { AppFileSystem } from "@/filesystem"
|
||||
import { Truncate } from "@/tool/truncate"
|
||||
import { decodeDataUrl } from "@/util/data-url"
|
||||
|
|
@ -791,13 +791,12 @@ 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 = ShellTool.from(name)
|
||||
const part: MessageV2.ToolPart = {
|
||||
type: "tool",
|
||||
id: PartID.ascending(),
|
||||
messageID: msg.id,
|
||||
sessionID: input.sessionID,
|
||||
tool,
|
||||
tool: ShellToolID.id,
|
||||
callID: ulid(),
|
||||
state: {
|
||||
status: "running",
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ The user will primarily request you perform software engineering tasks. This inc
|
|||
- When WebFetch returns a message about a redirect to a different host, you should immediately make a new WebFetch request with the redirect URL provided in the response.
|
||||
- You can call multiple tools in a single response. If you intend to call multiple tools and there are no dependencies between them, make all independent tool calls in parallel. Maximize use of parallel tool calls where possible to increase efficiency. However, if some tool calls depend on previous calls to inform dependent values, do NOT call these tools in parallel and instead call them sequentially. For instance, if one operation must complete before another starts, run these operations sequentially instead. Never use placeholders or guess missing parameters in tool calls.
|
||||
- If the user specifies that they want you to run tools "in parallel", you MUST send a single message with multiple tool use content blocks. For example, if you need to launch multiple agents in parallel, send a single message with multiple Task tool calls.
|
||||
- Use specialized tools instead of bash commands when possible, as this provides a better user experience. For file operations, use dedicated tools: Read for reading files instead of cat/head/tail, Edit for editing instead of sed/awk, and Write for creating files instead of cat with heredoc or echo redirection. Reserve bash tools exclusively for actual system commands and terminal operations that require shell execution. NEVER use bash echo or other command-line tools to communicate thoughts, explanations, or instructions to the user. Output all communication directly in your response text instead.
|
||||
- Use specialized tools instead of shell commands when possible, as this provides a better user experience. For file operations, use dedicated tools: Read for reading files instead of cat/head/tail, Edit for editing instead of sed/awk, and Write for creating files instead of cat with heredoc or echo redirection. Reserve the shell tool exclusively for actual system commands and terminal operations that require shell execution. NEVER use shell echo or other command-line tools to communicate thoughts, explanations, or instructions to the user. Output all communication directly in your response text instead.
|
||||
- VERY IMPORTANT: When exploring the codebase to gather context or to answer a question that is not a needle query for a specific file/class/function, it is CRITICAL that you use the Task tool instead of running search commands directly.
|
||||
<example>
|
||||
user: Where are errors from the client handled?
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ If the user asks for help or wants to give feedback inform them of the following
|
|||
When the user directly asks about opencode (eg 'can opencode do...', 'does opencode have...') or asks in second person (eg 'are you able...', 'can you do...'), first use the WebFetch tool to gather information to answer the question from opencode docs at https://opencode.ai
|
||||
|
||||
# Tone and style
|
||||
You should be concise, direct, and to the point. When you run a non-trivial bash command, you should explain what the command does and why you are running it, to make sure the user understands what you are doing (this is especially important when you are running a command that will make changes to the user's system).
|
||||
You should be concise, direct, and to the point. When you run a non-trivial shell command, you should explain what the command does and why you are running it, to make sure the user understands what you are doing (this is especially important when you are running a command that will make changes to the user's system).
|
||||
Remember that your output will be displayed on a command line interface. Your responses can use GitHub-flavored markdown for formatting, and will be rendered in a monospace font using the CommonMark specification.
|
||||
Output text to communicate with the user; all text you output outside of tool use is displayed to the user. Only use tools to complete tasks. Never use tools like Bash or code comments as means to communicate with the user during the session.
|
||||
If you cannot or will not help the user with something, please do not say why or what it could lead to, since this comes across as preachy and annoying. Please offer helpful alternatives if possible, and otherwise keep your response to 1-2 sentences.
|
||||
|
|
@ -89,7 +89,7 @@ NEVER commit changes unless the user explicitly asks you to. It is VERY IMPORTAN
|
|||
|
||||
# Tool usage policy
|
||||
- When doing file search, prefer to use the Task tool in order to reduce context usage.
|
||||
- You have the capability to call multiple tools in a single response. When multiple independent pieces of information are requested, batch your tool calls together for optimal performance. When making multiple bash tool calls, you MUST send a single message with multiple tools calls to run the calls in parallel. For example, if you need to run "git status" and "git diff", send a single message with two tool calls to run the calls in parallel.
|
||||
- You have the capability to call multiple tools in a single response. When multiple independent pieces of information are requested, batch your tool calls together for optimal performance. When making multiple shell tool calls, you MUST send a single message with multiple tools calls to run the calls in parallel. For example, if you need to run "git status" and "git diff", send a single message with two tool calls to run the calls in parallel.
|
||||
|
||||
You MUST answer concisely with fewer than 4 lines of text (not including tool use or code generation), unless user asks for detail.
|
||||
|
||||
|
|
|
|||
|
|
@ -19,18 +19,18 @@ You are opencode, an interactive CLI agent specializing in software engineering
|
|||
When requested to perform tasks like fixing bugs, adding features, refactoring, or explaining code, follow this sequence:
|
||||
1. **Understand:** Think about the user's request and the relevant codebase context. Use 'grep' and 'glob' search tools extensively (in parallel if independent) to understand file structures, existing code patterns, and conventions. Use 'read' to understand context and validate any assumptions you may have.
|
||||
2. **Plan:** Build a coherent and grounded (based on the understanding in step 1) plan for how you intend to resolve the user's task. Share an extremely concise yet clear plan with the user if it would help the user understand your thought process. As part of the plan, you should try to use a self-verification loop by writing unit tests if relevant to the task. Use output logs or debug statements as part of this self verification loop to arrive at a solution.
|
||||
3. **Implement:** Use the available tools (e.g., 'edit', 'write' 'bash' ...) to act on the plan, strictly adhering to the project's established conventions (detailed under 'Core Mandates').
|
||||
3. **Implement:** Use the available tools (e.g., 'edit', 'write' 'shell' ...) to act on the plan, strictly adhering to the project's established conventions (detailed under 'Core Mandates').
|
||||
4. **Verify (Tests):** If applicable and feasible, verify the changes using the project's testing procedures. Identify the correct test commands and frameworks by examining 'README' files, build/package configuration (e.g., 'package.json'), or existing test execution patterns. NEVER assume standard test commands.
|
||||
5. **Verify (Standards):** VERY IMPORTANT: After making code changes, execute the project-specific build, linting and type-checking commands (e.g., 'tsc', 'npm run lint', 'ruff check .') that you have identified for this project (or obtained from the user). This ensures code quality and adherence to standards. If unsure about these commands, you can ask the user if they'd like you to run them and if so how to.
|
||||
|
||||
## New Applications
|
||||
|
||||
**Goal:** Autonomously implement and deliver a visually appealing, substantially complete, and functional prototype. Utilize all tools at your disposal to implement the application. Some tools you may especially find useful are 'write', 'edit' and 'bash'.
|
||||
**Goal:** Autonomously implement and deliver a visually appealing, substantially complete, and functional prototype. Utilize all tools at your disposal to implement the application. Some tools you may especially find useful are 'write', 'edit' and 'shell'.
|
||||
|
||||
1. **Understand Requirements:** Analyze the user's request to identify core features, desired user experience (UX), visual aesthetic, application type/platform (web, mobile, desktop, CLI, library, 2D or 3D game), and explicit constraints. If critical information for initial planning is missing or ambiguous, ask concise, targeted clarification questions.
|
||||
2. **Propose Plan:** Formulate an internal development plan. Present a clear, concise, high-level summary to the user. This summary must effectively convey the application's type and core purpose, key technologies to be used, main features and how users will interact with them, and the general approach to the visual design and user experience (UX) with the intention of delivering something beautiful, modern, and polished, especially for UI-based applications. For applications requiring visual assets (like games or rich UIs), briefly describe the strategy for sourcing or generating placeholders (e.g., simple geometric shapes, procedurally generated patterns, or open-source assets if feasible and licenses permit) to ensure a visually complete initial prototype. Ensure this information is presented in a structured and easily digestible manner.
|
||||
3. **User Approval:** Obtain user approval for the proposed plan.
|
||||
4. **Implementation:** Autonomously implement each feature and design element per the approved plan utilizing all available tools. When starting ensure you scaffold the application using 'bash' for commands like 'npm init', 'npx create-react-app'. Aim for full scope completion. Proactively create or source necessary placeholder assets (e.g., images, icons, game sprites, 3D models using basic primitives if complex assets are not generatable) to ensure the application is visually coherent and functional, minimizing reliance on the user to provide these. If the model can generate simple assets (e.g., a uniformly colored square sprite, a simple 3D cube), it should do so. Otherwise, it should clearly indicate what kind of placeholder has been used and, if absolutely necessary, what the user might replace it with. Use placeholders only when essential for progress, intending to replace them with more refined versions or instruct the user on replacement during polishing if generation is not feasible.
|
||||
4. **Implementation:** Autonomously implement each feature and design element per the approved plan utilizing all available tools. When starting ensure you scaffold the application using 'shell' for commands like 'npm init', 'npx create-react-app'. Aim for full scope completion. Proactively create or source necessary placeholder assets (e.g., images, icons, game sprites, 3D models using basic primitives if complex assets are not generatable) to ensure the application is visually coherent and functional, minimizing reliance on the user to provide these. If the model can generate simple assets (e.g., a uniformly colored square sprite, a simple 3D cube), it should do so. Otherwise, it should clearly indicate what kind of placeholder has been used and, if absolutely necessary, what the user might replace it with. Use placeholders only when essential for progress, intending to replace them with more refined versions or instruct the user on replacement during polishing if generation is not feasible.
|
||||
5. **Verify:** Review work against the original request, the approved plan. Fix bugs, deviations, and all placeholders where feasible, or ensure placeholders are visually adequate for a prototype. Ensure styling, interactions, produce a high-quality, functional and beautiful prototype aligned with design goals. Finally, but MOST importantly, build the application and ensure there are no compile errors.
|
||||
6. **Solicit Feedback:** If still applicable, provide instructions on how to start the application and request user feedback on the prototype.
|
||||
|
||||
|
|
@ -46,13 +46,13 @@ When requested to perform tasks like fixing bugs, adding features, refactoring,
|
|||
- **Handling Inability:** If unable/unwilling to fulfill a request, state so briefly (1-2 sentences) without excessive justification. Offer alternatives if appropriate.
|
||||
|
||||
## Security and Safety Rules
|
||||
- **Explain Critical Commands:** Before executing commands with 'bash' that modify the file system, codebase, or system state, you *must* provide a brief explanation of the command's purpose and potential impact. Prioritize user understanding and safety. You should not ask permission to use the tool; the user will be presented with a confirmation dialogue upon use (you do not need to tell them this).
|
||||
- **Explain Critical Commands:** Before executing commands with 'shell' that modify the file system, codebase, or system state, you *must* provide a brief explanation of the command's purpose and potential impact. Prioritize user understanding and safety. You should not ask permission to use the tool; the user will be presented with a confirmation dialogue upon use (you do not need to tell them this).
|
||||
- **Security First:** Always apply security best practices. Never introduce code that exposes, logs, or commits secrets, API keys, or other sensitive information.
|
||||
|
||||
## Tool Usage
|
||||
- **File Paths:** Always use absolute paths when referring to files with tools like 'read' or 'write'. Relative paths are not supported. You must provide an absolute path.
|
||||
- **Parallelism:** Execute multiple independent tool calls in parallel when feasible (i.e. searching the codebase).
|
||||
- **Command Execution:** Use the 'bash' tool for running shell commands, remembering the safety rule to explain modifying commands first.
|
||||
- **Command Execution:** Use the 'shell' tool for running shell commands, remembering the safety rule to explain modifying commands first.
|
||||
- **Background Processes:** Use background processes (via \`&\`) for commands that are unlikely to stop on their own, e.g. \`node server.js &\`. If unsure, ask the user.
|
||||
- **Interactive Commands:** Try to avoid shell commands that are likely to require user interaction (e.g. \`git rebase -i\`). Use non-interactive versions of commands (e.g. \`npm init -y\` instead of \`npm init\`) when available, and otherwise remind the user that interactive shell commands are not supported and may cause hangs until canceled by the user.
|
||||
- **Respect User Confirmations:** Most tool calls (also denoted as 'function calls') will first require confirmation from the user, where they will either approve or cancel the function call. If a user cancels a function call, respect their choice and do _not_ try to make the function call again. It is okay to request the tool call again _only_ if the user requests that same tool call on a subsequent prompt. When a user cancels a function call, assume best intentions from the user and consider inquiring if they prefer any alternative paths forward.
|
||||
|
|
@ -79,7 +79,7 @@ model: [tool_call: ls for path '/path/to/project']
|
|||
|
||||
<example>
|
||||
user: start the server implemented in server.js
|
||||
model: [tool_call: bash for 'node server.js &' because it must run in the background]
|
||||
model: [tool_call: shell for 'node server.js &' because it must run in the background]
|
||||
</example>
|
||||
|
||||
<example>
|
||||
|
|
@ -106,7 +106,7 @@ user: Yes
|
|||
model:
|
||||
[tool_call: write or edit to apply the refactoring to 'src/auth.py']
|
||||
Refactoring complete. Running verification...
|
||||
[tool_call: bash for 'ruff check src/auth.py && pytest']
|
||||
[tool_call: shell for 'ruff check src/auth.py && pytest']
|
||||
(After verification passes)
|
||||
All checks passed. This is a stable checkpoint.
|
||||
|
||||
|
|
@ -125,7 +125,7 @@ Now I'll look for existing or related test files to understand current testing c
|
|||
(After reviewing existing tests and the file content)
|
||||
[tool_call: write to create /path/to/someFile.test.ts with the test code]
|
||||
I've written the tests. Now I'll run the project's test command to verify them.
|
||||
[tool_call: bash for 'npm run test']
|
||||
[tool_call: shell for 'npm run test']
|
||||
</example>
|
||||
|
||||
<example>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ You are OpenCode, You and the user share the same workspace and collaborate to a
|
|||
You are a deeply pragmatic, effective software engineer. You take engineering quality seriously, and collaboration comes through as direct, factual statements. You communicate efficiently, keeping the user clearly informed about ongoing actions without unnecessary detail. You build context by examining the codebase first without making assumptions or jumping to conclusions. You think through the nuances of the code you encounter, and embody the mentality of a skilled senior software engineer.
|
||||
|
||||
- When searching for text or files, prefer using Glob and Grep tools (they are powered by `rg`)
|
||||
- Parallelize tool calls whenever possible - especially file reads. Use `multi_tool_use.parallel` to parallelize tool calls and only this. Never chain together bash commands with separators like `echo "====";` as this renders to the user poorly.
|
||||
- Parallelize tool calls whenever possible - especially file reads. Use `multi_tool_use.parallel` to parallelize tool calls and only this. Never chain together shell commands with separators like `echo "====";` as this renders to the user poorly.
|
||||
|
||||
## Editing Approach
|
||||
|
||||
|
|
|
|||
|
|
@ -30,8 +30,8 @@ When building something from scratch, you should:
|
|||
Always use tools to implement your code changes:
|
||||
|
||||
- Use `write`/`edit` to create or modify source files. Code that only appears in your text response is NOT saved to the file system and will not take effect.
|
||||
- Use `bash` to run and test your code after writing it.
|
||||
- Iterate: if tests fail, read the error, fix the code with `write`/`edit`, and re-test with `bash`.
|
||||
- Use `shell` to run and test your code after writing it.
|
||||
- Iterate: if tests fail, read the error, fix the code with `write`/`edit`, and re-test with `shell`.
|
||||
|
||||
When working on an existing codebase, you should:
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
CRITICAL: Plan mode ACTIVE - you are in READ-ONLY phase. STRICTLY FORBIDDEN:
|
||||
ANY file edits, modifications, or system changes. Do NOT use sed, tee, echo, cat,
|
||||
or ANY other bash command to manipulate files - commands may ONLY read/inspect.
|
||||
or ANY other shell command to manipulate files - commands may ONLY read/inspect.
|
||||
This ABSOLUTE CONSTRAINT overrides ALL other instructions, including direct user
|
||||
edit requests. You may ONLY observe, analyze, and plan. Any modification attempt
|
||||
is a critical violation. ZERO exceptions.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
You are opencode, an interactive CLI tool that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user.
|
||||
|
||||
# Tone and style
|
||||
You should be concise, direct, and to the point. When you run a non-trivial bash command, you should explain what the command does and why you are running it, to make sure the user understands what you are doing (this is especially important when you are running a command that will make changes to the user's system).
|
||||
You should be concise, direct, and to the point. When you run a non-trivial shell command, you should explain what the command does and why you are running it, to make sure the user understands what you are doing (this is especially important when you are running a command that will make changes to the user's system).
|
||||
Remember that your output will be displayed on a command line interface. Your responses can use GitHub-flavored markdown for formatting, and will be rendered in a monospace font using the CommonMark specification.
|
||||
Output text to communicate with the user; all text you output outside of tool use is displayed to the user. Only use tools to complete tasks. Never use tools like Bash or code comments as means to communicate with the user during the session.
|
||||
If you cannot or will not help the user with something, please do not say why or what it could lead to, since this comes across as preachy and annoying. Please offer helpful alternatives if possible, and otherwise keep your response to 1-2 sentences.
|
||||
|
|
|
|||
|
|
@ -29,11 +29,8 @@ import { pathToFileURL } from "url"
|
|||
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"
|
||||
import { ShellTool } from "./shell/tool"
|
||||
import { ShellToolID } from "./shell/id"
|
||||
import { Env } from "../env"
|
||||
import { Question } from "../question"
|
||||
import { Todo } from "../session/todo"
|
||||
|
|
@ -45,7 +42,6 @@ import { Agent } from "../agent/agent"
|
|||
|
||||
export namespace ToolRegistry {
|
||||
const log = Log.create({ service: "tool.registry" })
|
||||
const shells = { bash: BashTool, pwsh: PwshTool, powershell: PowershellTool } as const
|
||||
|
||||
type State = {
|
||||
custom: Tool.Def[]
|
||||
|
|
@ -138,14 +134,13 @@ export namespace ToolRegistry {
|
|||
|
||||
const question =
|
||||
["app", "cli", "desktop"].includes(Flag.OPENCODE_CLIENT) || Flag.OPENCODE_ENABLE_QUESTION_TOOL
|
||||
const active = shells[ShellTool.from(Shell.name(Shell.acceptable()))]
|
||||
|
||||
return {
|
||||
custom,
|
||||
builtin: yield* Effect.forEach(
|
||||
[
|
||||
InvalidTool,
|
||||
active,
|
||||
ShellTool,
|
||||
ReadTool,
|
||||
GlobTool,
|
||||
GrepTool,
|
||||
|
|
@ -176,7 +171,7 @@ export namespace ToolRegistry {
|
|||
|
||||
const fromID: Interface["fromID"] = Effect.fn("ToolRegistry.fromID")(function* (id: string) {
|
||||
const tools = yield* all()
|
||||
const match = tools.find((tool) => tool.id === id)
|
||||
const match = tools.find((tool) => tool.id === ShellToolID.normalize(id))
|
||||
if (!match) return yield* Effect.die(`Tool not found: ${id}`)
|
||||
return match
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { ShellTool } from "./id"
|
||||
import { ShellKind } from "./id"
|
||||
|
||||
export namespace ShellArity {
|
||||
export function prefix(tokens: string[], shellType: ShellTool.ID) {
|
||||
if (ShellTool.powershell(shellType) && tokens.length > 0 && /^[a-z]+-[a-z]+$/i.test(tokens[0])) {
|
||||
export function prefix(tokens: string[], shellType: ShellKind.ID) {
|
||||
if (ShellKind.powershell(shellType) && tokens.length > 0 && /^[a-z]+-[a-z]+$/i.test(tokens[0])) {
|
||||
return [tokens[0]]
|
||||
}
|
||||
for (let len = tokens.length; len > 0; len--) {
|
||||
|
|
|
|||
|
|
@ -1,12 +0,0 @@
|
|||
import { createShellTool } from "./util"
|
||||
|
||||
export const BashTool = createShellTool({
|
||||
id: "bash",
|
||||
shellName: "bash",
|
||||
toolName: "Bash",
|
||||
listCmd: "ls",
|
||||
gitCmds: "git bash commands",
|
||||
chaining:
|
||||
"use a single Bash call with '&&' to chain them together (e.g., `git add . && git commit -m \"message\" && git push`).",
|
||||
guidance: "",
|
||||
})
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
export namespace ShellTool {
|
||||
export namespace ShellKind {
|
||||
export const ids = ["bash", "pwsh", "powershell"] as const
|
||||
export type ID = (typeof ids)[number]
|
||||
|
||||
const shell = new Set<string>(ids)
|
||||
const kind = new Set<string>(ids)
|
||||
const ps = new Set<string>(["pwsh", "powershell"])
|
||||
|
||||
export function has(value: string): value is ID {
|
||||
return shell.has(value)
|
||||
return kind.has(value)
|
||||
}
|
||||
|
||||
export function from(value: string): ID {
|
||||
|
|
@ -17,3 +17,19 @@ export namespace ShellTool {
|
|||
return ps.has(value)
|
||||
}
|
||||
}
|
||||
|
||||
export namespace ShellToolID {
|
||||
export const id = "shell"
|
||||
export const legacy = "bash"
|
||||
export type ID = typeof id | typeof legacy
|
||||
|
||||
const tool = new Set<string>([id, legacy])
|
||||
|
||||
export function has(value: string): value is ID {
|
||||
return tool.has(value)
|
||||
}
|
||||
|
||||
export function normalize(value: string) {
|
||||
return value === legacy ? id : value
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +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 { ShellKind } from "./id"
|
||||
import { Instance } from "@/project/instance"
|
||||
import { Filesystem } from "@/util/filesystem"
|
||||
import path from "path"
|
||||
|
|
@ -165,9 +165,9 @@ export namespace ShellParser {
|
|||
command: string
|
||||
cwd: string
|
||||
shell: string
|
||||
shellType: ShellTool.ID
|
||||
shellType: ShellKind.ID
|
||||
}): Promise<Scan> {
|
||||
const isPwsh = ShellTool.powershell(opts.shellType)
|
||||
const isPwsh = ShellKind.powershell(opts.shellType)
|
||||
const parser = isPwsh ? await getPsParser() : await getBashParser()
|
||||
|
||||
const tree = parser.parse(opts.command)
|
||||
|
|
|
|||
|
|
@ -1,18 +0,0 @@
|
|||
import { createShellTool } from "./util"
|
||||
|
||||
export const PowershellTool = createShellTool({
|
||||
id: "powershell",
|
||||
shellName: "Windows PowerShell",
|
||||
toolName: "PowerShell",
|
||||
listCmd: "Get-ChildItem",
|
||||
gitCmds: "git commands",
|
||||
chaining:
|
||||
"use PowerShell conditionals such as `cmd1; if ($?) { cmd2 }` when later commands must depend on earlier success.",
|
||||
guidance: `# Windows PowerShell 5.1 shell notes
|
||||
- Use \`cmd1; if ($?) { cmd2 }\` to chain dependent commands.
|
||||
- Use double quotes for interpolated strings (\`"Hello $name"\`), single quotes for verbatim strings.
|
||||
- Cmdlets use Verb-Noun naming (e.g., \`Get-ChildItem\`, \`Set-Content\`). Common aliases like \`ls\`, \`cat\`, \`rm\` execute the equivalent PowerShell cmdlets.
|
||||
- Use \`$(...)\` for subexpressions. Use \`@(...)\` for array expressions.
|
||||
- To call a native executable whose path contains spaces, use the call operator: \`& "path/to/exe" args\`.
|
||||
- Escape special characters with backtick (\\\`).`,
|
||||
})
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
import { createShellTool } from "./util"
|
||||
|
||||
export const PwshTool = createShellTool({
|
||||
id: "pwsh",
|
||||
shellName: "PowerShell Core",
|
||||
toolName: "PowerShell",
|
||||
listCmd: "Get-ChildItem",
|
||||
gitCmds: "git commands",
|
||||
chaining:
|
||||
"use a single PowerShell call with '&&' to chain them together (e.g., `git add . && git commit -m \"message\" && git push`).",
|
||||
guidance: `# PowerShell 7+ (pwsh) shell notes
|
||||
- This cross-platform shell supports pipeline chain operators (\`&&\` and \`||\`).
|
||||
- Use double quotes for interpolated strings (\`"Hello $name"\`), single quotes for verbatim strings.
|
||||
- Cmdlets use Verb-Noun naming (e.g., \`Get-ChildItem\`, \`Set-Content\`). Common aliases like \`ls\`, \`cat\`, \`rm\` execute the equivalent PowerShell cmdlets.
|
||||
- Use \`$(...)\` for subexpressions. Use \`@(...)\` for array expressions.
|
||||
- To call a native executable whose path contains spaces, use the call operator: \`& "path/to/exe" args\`.
|
||||
- Escape special characters with backtick (\\\`).`,
|
||||
})
|
||||
|
|
@ -2,7 +2,7 @@ import { spawn } from "child_process"
|
|||
import { Shell } from "@/shell/shell"
|
||||
import { Tool } from "../tool"
|
||||
import { Plugin } from "@/plugin"
|
||||
import { ShellTool } from "./id"
|
||||
import { ShellKind } from "./id"
|
||||
|
||||
const MAX_METADATA_LENGTH = 30_000
|
||||
|
||||
|
|
@ -27,8 +27,8 @@ exit 1`
|
|||
}
|
||||
}
|
||||
|
||||
export function launch(shell: string, name: string, command: string, cwd: string, env: NodeJS.ProcessEnv) {
|
||||
if (process.platform === "win32" && ShellTool.powershell(name)) {
|
||||
export function launch(shell: string, kind: ShellKind.ID, command: string, cwd: string, env: NodeJS.ProcessEnv) {
|
||||
if (process.platform === "win32" && ShellKind.powershell(kind)) {
|
||||
return spawn(shell, ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", preserveExitCode(command)], {
|
||||
cwd,
|
||||
env,
|
||||
|
|
@ -51,7 +51,7 @@ exit 1`
|
|||
export async function run(
|
||||
input: {
|
||||
shell: string
|
||||
name: string
|
||||
kind: ShellKind.ID
|
||||
command: string
|
||||
cwd: string
|
||||
env: NodeJS.ProcessEnv
|
||||
|
|
@ -60,7 +60,7 @@ exit 1`
|
|||
},
|
||||
ctx: Tool.Context,
|
||||
) {
|
||||
const proc = launch(input.shell, input.name, input.command, input.cwd, input.env)
|
||||
const proc = launch(input.shell, input.kind, input.command, input.cwd, input.env)
|
||||
let output = ""
|
||||
let code: number | null = null
|
||||
|
||||
|
|
@ -135,7 +135,7 @@ exit 1`
|
|||
await wait
|
||||
|
||||
const metadata: string[] = []
|
||||
if (expired) metadata.push(`${input.name} tool terminated command after exceeding timeout ${input.timeout} ms`)
|
||||
if (expired) metadata.push(`shell tool terminated command after exceeding timeout ${input.timeout} ms`)
|
||||
if (aborted) metadata.push("User aborted the command")
|
||||
if (metadata.length > 0) {
|
||||
output += "\n\n<shell_metadata>\n" + metadata.join("\n") + "\n</shell_metadata>"
|
||||
|
|
|
|||
3
packages/opencode/src/tool/shell/tool.ts
Normal file
3
packages/opencode/src/tool/shell/tool.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import { createShellTool } from "./util"
|
||||
|
||||
export const ShellTool = createShellTool()
|
||||
|
|
@ -108,41 +108,81 @@ export function formatShellDescription(
|
|||
|
||||
import z from "zod"
|
||||
import DESCRIPTION from "./shell.txt"
|
||||
import { ShellTool } from "./id"
|
||||
import { ShellKind, ShellToolID } from "./id"
|
||||
import { Log } from "@/util/log"
|
||||
import { Flag } from "@/flag/flag"
|
||||
import { ShellParser } from "./parser"
|
||||
import { ShellRunner } from "./runner"
|
||||
|
||||
export type ShellType = ShellTool.ID
|
||||
|
||||
const DEFAULT_TIMEOUT = Flag.OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS || 2 * 60 * 1000
|
||||
|
||||
export function createShellTool(opts: {
|
||||
id: ShellType
|
||||
shellName: string
|
||||
chaining: string
|
||||
guidance: string
|
||||
listCmd: string
|
||||
toolName: string
|
||||
gitCmds: string
|
||||
}) {
|
||||
const log = Log.create({ service: `${opts.id}-tool` })
|
||||
const info = {
|
||||
bash: {
|
||||
shellName: "bash",
|
||||
listCmd: "ls",
|
||||
gitCmds: "git commands",
|
||||
chaining:
|
||||
"use a single shell call with '&&' to chain them together (e.g., `git add . && git commit -m \"message\" && git push`).",
|
||||
guidance: "",
|
||||
},
|
||||
pwsh: {
|
||||
shellName: "PowerShell Core",
|
||||
listCmd: "Get-ChildItem",
|
||||
gitCmds: "git commands",
|
||||
chaining:
|
||||
"use a single shell call with '&&' to chain them together (e.g., `git add . && git commit -m \"message\" && git push`).",
|
||||
guidance: `# PowerShell 7+ (pwsh) shell notes
|
||||
- This cross-platform shell supports pipeline chain operators (\`&&\` and \`||\`).
|
||||
- Use double quotes for interpolated strings (\`"Hello $name"\`), single quotes for verbatim strings.
|
||||
- Cmdlets use Verb-Noun naming (e.g., \`Get-ChildItem\`, \`Set-Content\`). Common aliases like \`ls\`, \`cat\`, \`rm\` execute the equivalent PowerShell cmdlets.
|
||||
- Use \`$(...)\` for subexpressions. Use \`@(...)\` for array expressions.
|
||||
- To call a native executable whose path contains spaces, use the call operator: \`& "path/to/exe" args\`.
|
||||
- Escape special characters with backtick (\\\`).`,
|
||||
},
|
||||
powershell: {
|
||||
shellName: "Windows PowerShell",
|
||||
listCmd: "Get-ChildItem",
|
||||
gitCmds: "git commands",
|
||||
chaining:
|
||||
"use shell conditionals such as `cmd1; if ($?) { cmd2 }` when later commands must depend on earlier success.",
|
||||
guidance: `# Windows PowerShell 5.1 shell notes
|
||||
- Use \`cmd1; if ($?) { cmd2 }\` to chain dependent commands.
|
||||
- Use double quotes for interpolated strings (\`"Hello $name"\`), single quotes for verbatim strings.
|
||||
- Cmdlets use Verb-Noun naming (e.g., \`Get-ChildItem\`, \`Set-Content\`). Common aliases like \`ls\`, \`cat\`, \`rm\` execute the equivalent PowerShell cmdlets.
|
||||
- Use \`$(...)\` for subexpressions. Use \`@(...)\` for array expressions.
|
||||
- To call a native executable whose path contains spaces, use the call operator: \`& "path/to/exe" args\`.
|
||||
- Escape special characters with backtick (\\\`).`,
|
||||
},
|
||||
} satisfies Record<
|
||||
ShellKind.ID,
|
||||
{
|
||||
shellName: string
|
||||
listCmd: string
|
||||
gitCmds: string
|
||||
chaining: string
|
||||
guidance: string
|
||||
}
|
||||
>
|
||||
|
||||
return Tool.define(opts.id, async () => {
|
||||
export function createShellTool() {
|
||||
const log = Log.create({ service: "shell-tool" })
|
||||
|
||||
return Tool.define(ShellToolID.id, async () => {
|
||||
const shell = Shell.acceptable()
|
||||
const name = Shell.name(shell)
|
||||
log.info(`${opts.id} tool using shell`, { shell, name })
|
||||
const kind = ShellKind.from(name)
|
||||
const cfg = info[kind]
|
||||
log.info("shell tool using shell", { shell, name, kind })
|
||||
|
||||
return {
|
||||
description: formatShellDescription(DESCRIPTION, {
|
||||
name,
|
||||
shellName: opts.shellName,
|
||||
chaining: opts.chaining,
|
||||
guidance: opts.guidance,
|
||||
listCmd: opts.listCmd,
|
||||
toolName: opts.toolName,
|
||||
gitCmds: opts.gitCmds,
|
||||
shellName: cfg.shellName,
|
||||
chaining: cfg.chaining,
|
||||
guidance: cfg.guidance,
|
||||
listCmd: cfg.listCmd,
|
||||
toolName: "Shell",
|
||||
gitCmds: cfg.gitCmds,
|
||||
}),
|
||||
parameters: z.object({
|
||||
command: z.string().describe("The command to execute"),
|
||||
|
|
@ -170,16 +210,16 @@ export function createShellTool(opts: {
|
|||
command: params.command,
|
||||
cwd,
|
||||
shell,
|
||||
shellType: opts.id,
|
||||
shellType: kind,
|
||||
})
|
||||
if (!Instance.containsPath(cwd)) scan.dirs.add(cwd)
|
||||
|
||||
await askPermission(ctx, scan, opts.id)
|
||||
await askPermission(ctx, scan, ShellToolID.id)
|
||||
|
||||
return ShellRunner.run(
|
||||
{
|
||||
shell,
|
||||
name,
|
||||
kind,
|
||||
command: params.command,
|
||||
cwd,
|
||||
env: await ShellRunner.shellEnv(ctx, cwd),
|
||||
|
|
@ -193,7 +233,7 @@ export function createShellTool(opts: {
|
|||
})
|
||||
}
|
||||
|
||||
export async function askPermission(ctx: Tool.Context, scan: Scan, permissionName: string = "bash") {
|
||||
export async function askPermission(ctx: Tool.Context, scan: Scan, permissionName: string = ShellToolID.id) {
|
||||
if (scan.dirs.size > 0) {
|
||||
const globs = Array.from(scan.dirs).map((dir) => {
|
||||
if (process.platform === "win32") return Filesystem.normalizePathPattern(path.join(dir, "*"))
|
||||
|
|
|
|||
|
|
@ -391,7 +391,7 @@ describe("acp.agent event subscription", () => {
|
|||
properties: {
|
||||
id: "perm_1",
|
||||
sessionID: sessionA,
|
||||
permission: "bash",
|
||||
permission: "shell",
|
||||
patterns: ["*"],
|
||||
metadata: {},
|
||||
always: [],
|
||||
|
|
@ -450,7 +450,7 @@ describe("acp.agent event subscription", () => {
|
|||
properties: {
|
||||
id: "perm_a",
|
||||
sessionID: sessionA,
|
||||
permission: "bash",
|
||||
permission: "shell",
|
||||
patterns: ["*"],
|
||||
metadata: {},
|
||||
always: [],
|
||||
|
|
@ -509,7 +509,7 @@ describe("acp.agent event subscription", () => {
|
|||
controller.push(
|
||||
toolEvent(sessionId, cwd, {
|
||||
callID: "call_1",
|
||||
tool: "bash",
|
||||
tool: "shell",
|
||||
status: "running",
|
||||
input,
|
||||
metadata: { output },
|
||||
|
|
@ -541,7 +541,7 @@ describe("acp.agent event subscription", () => {
|
|||
controller.push(
|
||||
toolEvent(sessionId, cwd, {
|
||||
callID: "call_bash",
|
||||
tool: "bash",
|
||||
tool: "shell",
|
||||
status: "running",
|
||||
input: { command: "echo hi", description: "run command" },
|
||||
metadata: { output: "hi\n" },
|
||||
|
|
@ -595,7 +595,7 @@ describe("acp.agent event subscription", () => {
|
|||
{
|
||||
type: "tool",
|
||||
callID: "call_1",
|
||||
tool: "bash",
|
||||
tool: "shell",
|
||||
state: {
|
||||
status: "running",
|
||||
input,
|
||||
|
|
@ -612,7 +612,7 @@ describe("acp.agent event subscription", () => {
|
|||
controller.push(
|
||||
toolEvent(sessionId, cwd, {
|
||||
callID: "call_1",
|
||||
tool: "bash",
|
||||
tool: "shell",
|
||||
status: "running",
|
||||
input,
|
||||
metadata: { output: "hi\nthere\n" },
|
||||
|
|
@ -646,7 +646,7 @@ describe("acp.agent event subscription", () => {
|
|||
controller.push(
|
||||
toolEvent(sessionId, cwd, {
|
||||
callID: "call_1",
|
||||
tool: "bash",
|
||||
tool: "shell",
|
||||
status: "running",
|
||||
input,
|
||||
metadata: { output: "a" },
|
||||
|
|
@ -655,7 +655,7 @@ describe("acp.agent event subscription", () => {
|
|||
controller.push(
|
||||
toolEvent(sessionId, cwd, {
|
||||
callID: "call_1",
|
||||
tool: "bash",
|
||||
tool: "shell",
|
||||
status: "pending",
|
||||
input,
|
||||
raw: '{"command":"echo hello"}',
|
||||
|
|
@ -664,7 +664,7 @@ describe("acp.agent event subscription", () => {
|
|||
controller.push(
|
||||
toolEvent(sessionId, cwd, {
|
||||
callID: "call_1",
|
||||
tool: "bash",
|
||||
tool: "shell",
|
||||
status: "running",
|
||||
input,
|
||||
metadata: { output: "a" },
|
||||
|
|
|
|||
|
|
@ -172,7 +172,7 @@ describe("transcript", () => {
|
|||
messageID: "msg_123",
|
||||
type: "tool",
|
||||
callID: "call_1",
|
||||
tool: "bash",
|
||||
tool: "shell",
|
||||
state: {
|
||||
status: "completed",
|
||||
input: { command: "ls" },
|
||||
|
|
@ -183,7 +183,7 @@ describe("transcript", () => {
|
|||
},
|
||||
}
|
||||
const result = formatPart(part, options)
|
||||
expect(result).toContain("**Tool: bash**")
|
||||
expect(result).toContain("**Tool: shell**")
|
||||
expect(result).toContain("**Input:**")
|
||||
expect(result).toContain('"command": "ls"')
|
||||
expect(result).toContain("**Output:**")
|
||||
|
|
@ -197,7 +197,7 @@ describe("transcript", () => {
|
|||
messageID: "msg_123",
|
||||
type: "tool",
|
||||
callID: "call_1",
|
||||
tool: "bash",
|
||||
tool: "shell",
|
||||
state: {
|
||||
status: "completed",
|
||||
input: { command: "echo '```hello```'" },
|
||||
|
|
@ -209,7 +209,7 @@ describe("transcript", () => {
|
|||
}
|
||||
const result = formatPart(part, options)
|
||||
// The tool header should not be inside a code block
|
||||
expect(result).toStartWith("**Tool: bash**\n")
|
||||
expect(result).toStartWith("**Tool: shell**\n")
|
||||
// Input and output should each be in their own code blocks
|
||||
expect(result).toContain("**Input:**\n```json")
|
||||
expect(result).toContain("**Output:**\n```\n```hello```\n```")
|
||||
|
|
@ -222,7 +222,7 @@ describe("transcript", () => {
|
|||
messageID: "msg_123",
|
||||
type: "tool",
|
||||
callID: "call_1",
|
||||
tool: "bash",
|
||||
tool: "shell",
|
||||
state: {
|
||||
status: "completed",
|
||||
input: { command: "ls" },
|
||||
|
|
@ -233,7 +233,7 @@ describe("transcript", () => {
|
|||
},
|
||||
}
|
||||
const result = formatPart(part, { ...options, toolDetails: false })
|
||||
expect(result).toContain("**Tool: bash**")
|
||||
expect(result).toContain("**Tool: shell**")
|
||||
expect(result).not.toContain("**Input:**")
|
||||
expect(result).not.toContain("**Output:**")
|
||||
})
|
||||
|
|
@ -245,7 +245,7 @@ describe("transcript", () => {
|
|||
messageID: "msg_123",
|
||||
type: "tool",
|
||||
callID: "call_1",
|
||||
tool: "bash",
|
||||
tool: "shell",
|
||||
state: {
|
||||
status: "error",
|
||||
input: { command: "invalid" },
|
||||
|
|
|
|||
|
|
@ -1266,7 +1266,7 @@ test("migrates legacy tools config to permissions - allow", async () => {
|
|||
fn: async () => {
|
||||
const config = await Config.get()
|
||||
expect(config.agent?.["test"]?.permission).toEqual({
|
||||
bash: "allow",
|
||||
shell: "allow",
|
||||
read: "allow",
|
||||
})
|
||||
},
|
||||
|
|
@ -1297,7 +1297,7 @@ test("migrates legacy tools config to permissions - deny", async () => {
|
|||
fn: async () => {
|
||||
const config = await Config.get()
|
||||
expect(config.agent?.["test"]?.permission).toEqual({
|
||||
bash: "deny",
|
||||
shell: "deny",
|
||||
webfetch: "deny",
|
||||
})
|
||||
},
|
||||
|
|
@ -1524,7 +1524,7 @@ test("migrates mixed legacy tools config", async () => {
|
|||
fn: async () => {
|
||||
const config = await Config.get()
|
||||
expect(config.agent?.["test"]?.permission).toEqual({
|
||||
bash: "allow",
|
||||
shell: "allow",
|
||||
edit: "allow",
|
||||
read: "deny",
|
||||
webfetch: "allow",
|
||||
|
|
@ -1560,7 +1560,7 @@ test("merges legacy tools with existing permission config", async () => {
|
|||
const config = await Config.get()
|
||||
expect(config.agent?.["test"]?.permission).toEqual({
|
||||
glob: "allow",
|
||||
bash: "allow",
|
||||
shell: "allow",
|
||||
})
|
||||
},
|
||||
})
|
||||
|
|
@ -2339,9 +2339,9 @@ test("parseManagedPlist parses permission rules", async () => {
|
|||
expect(config.permission?.grep).toBe("allow")
|
||||
expect(config.permission?.webfetch).toBe("ask")
|
||||
expect(config.permission?.["~/.ssh/*"]).toBe("deny")
|
||||
const bash = config.permission?.bash as Record<string, string>
|
||||
expect(bash?.["rm -rf *"]).toBe("deny")
|
||||
expect(bash?.["curl *"]).toBe("deny")
|
||||
const shell = config.permission?.shell as Record<string, string>
|
||||
expect(shell?.["rm -rf *"]).toBe("deny")
|
||||
expect(shell?.["curl *"]).toBe("deny")
|
||||
})
|
||||
|
||||
test("parseManagedPlist parses enabled_providers", async () => {
|
||||
|
|
|
|||
|
|
@ -34,22 +34,14 @@ async function waitForPending(count: number) {
|
|||
|
||||
test("fromConfig - string value becomes wildcard rule", () => {
|
||||
const result = Permission.fromConfig({ bash: "allow" })
|
||||
expect(result).toEqual([
|
||||
{ permission: "bash", pattern: "*", action: "allow" },
|
||||
{ permission: "pwsh", pattern: "*", action: "allow" },
|
||||
{ permission: "powershell", pattern: "*", action: "allow" },
|
||||
])
|
||||
expect(result).toEqual([{ permission: "shell", pattern: "*", action: "allow" }])
|
||||
})
|
||||
|
||||
test("fromConfig - object value converts to rules array", () => {
|
||||
const result = Permission.fromConfig({ bash: { "*": "allow", rm: "deny" } })
|
||||
expect(result).toEqual([
|
||||
{ permission: "bash", pattern: "*", action: "allow" },
|
||||
{ permission: "bash", pattern: "rm", action: "deny" },
|
||||
{ permission: "pwsh", pattern: "*", action: "allow" },
|
||||
{ permission: "pwsh", pattern: "rm", action: "deny" },
|
||||
{ permission: "powershell", pattern: "*", action: "allow" },
|
||||
{ permission: "powershell", pattern: "rm", action: "deny" },
|
||||
{ permission: "shell", pattern: "*", action: "allow" },
|
||||
{ permission: "shell", pattern: "rm", action: "deny" },
|
||||
])
|
||||
})
|
||||
|
||||
|
|
@ -60,39 +52,33 @@ test("fromConfig - mixed string and object values", () => {
|
|||
webfetch: "ask",
|
||||
})
|
||||
expect(result).toEqual([
|
||||
{ permission: "bash", pattern: "*", action: "allow" },
|
||||
{ permission: "bash", pattern: "rm", action: "deny" },
|
||||
{ permission: "pwsh", pattern: "*", action: "allow" },
|
||||
{ permission: "pwsh", pattern: "rm", action: "deny" },
|
||||
{ permission: "powershell", pattern: "*", action: "allow" },
|
||||
{ permission: "powershell", pattern: "rm", action: "deny" },
|
||||
{ permission: "shell", pattern: "*", action: "allow" },
|
||||
{ permission: "shell", pattern: "rm", action: "deny" },
|
||||
{ permission: "edit", pattern: "*", action: "allow" },
|
||||
{ permission: "webfetch", pattern: "*", action: "ask" },
|
||||
])
|
||||
})
|
||||
|
||||
test("fromConfig - explicit pwsh overrides bash regardless of key order", () => {
|
||||
test("fromConfig - shell and legacy bash normalize to shell in key order", () => {
|
||||
const result = Permission.fromConfig({
|
||||
pwsh: "deny",
|
||||
shell: "deny",
|
||||
bash: "allow",
|
||||
})
|
||||
expect(result).toEqual([
|
||||
{ permission: "bash", pattern: "*", action: "allow" },
|
||||
{ permission: "pwsh", pattern: "*", action: "allow" },
|
||||
{ permission: "powershell", pattern: "*", action: "allow" },
|
||||
{ permission: "pwsh", pattern: "*", action: "deny" },
|
||||
{ permission: "shell", pattern: "*", action: "deny" },
|
||||
{ permission: "shell", pattern: "*", action: "allow" },
|
||||
])
|
||||
expect(Permission.evaluate("pwsh", "ls", result).action).toBe("deny")
|
||||
expect(Permission.evaluate("bash", "ls", result).action).toBe("allow")
|
||||
expect(Permission.evaluate("shell", "ls", result).action).toBe("allow")
|
||||
})
|
||||
|
||||
test("fromConfig - explicit powershell pattern overrides bash pattern regardless of key order", () => {
|
||||
test("fromConfig - legacy bash rules coexist with canonical shell rules", () => {
|
||||
const result = Permission.fromConfig({
|
||||
powershell: { "rm *": "deny" },
|
||||
shell: { "rm *": "deny" },
|
||||
bash: { "*": "allow", "rm *": "ask" },
|
||||
})
|
||||
expect(Permission.evaluate("powershell", "rm foo", result).action).toBe("deny")
|
||||
expect(Permission.evaluate("pwsh", "rm foo", result).action).toBe("ask")
|
||||
expect(Permission.evaluate("shell", "rm foo", result).action).toBe("ask")
|
||||
expect(Permission.evaluate("bash", "rm foo", result).action).toBe("ask")
|
||||
})
|
||||
|
||||
test("fromConfig - empty object", () => {
|
||||
|
|
@ -234,6 +220,11 @@ test("evaluate - exact pattern match", () => {
|
|||
expect(result.action).toBe("deny")
|
||||
})
|
||||
|
||||
test("evaluate - shell matches legacy bash rules", () => {
|
||||
const result = Permission.evaluate("shell", "rm", [{ permission: "bash", pattern: "rm", action: "deny" }])
|
||||
expect(result.action).toBe("deny")
|
||||
})
|
||||
|
||||
test("evaluate - wildcard pattern match", () => {
|
||||
const result = Permission.evaluate("bash", "rm", [{ permission: "bash", pattern: "*", action: "allow" }])
|
||||
expect(result.action).toBe("allow")
|
||||
|
|
|
|||
|
|
@ -797,7 +797,7 @@ describe("ProviderTransform.message - DeepSeek reasoning content", () => {
|
|||
{
|
||||
type: "tool-call",
|
||||
toolCallId: "test",
|
||||
toolName: "bash",
|
||||
toolName: "shell",
|
||||
input: { command: "echo hello" },
|
||||
},
|
||||
],
|
||||
|
|
@ -848,7 +848,7 @@ describe("ProviderTransform.message - DeepSeek reasoning content", () => {
|
|||
{
|
||||
type: "tool-call",
|
||||
toolCallId: "test",
|
||||
toolName: "bash",
|
||||
toolName: "shell",
|
||||
input: { command: "echo hello" },
|
||||
},
|
||||
])
|
||||
|
|
@ -1125,7 +1125,7 @@ describe("ProviderTransform.message - anthropic empty content filtering", () =>
|
|||
role: "assistant",
|
||||
content: [
|
||||
{ type: "text", text: "" },
|
||||
{ type: "tool-call", toolCallId: "123", toolName: "bash", input: { command: "ls" } },
|
||||
{ type: "tool-call", toolCallId: "123", toolName: "shell", input: { command: "ls" } },
|
||||
],
|
||||
},
|
||||
] as any[]
|
||||
|
|
@ -1137,7 +1137,7 @@ describe("ProviderTransform.message - anthropic empty content filtering", () =>
|
|||
expect(result[0].content[0]).toEqual({
|
||||
type: "tool-call",
|
||||
toolCallId: "123",
|
||||
toolName: "bash",
|
||||
toolName: "shell",
|
||||
input: { command: "ls" },
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -470,7 +470,7 @@ describe("session.compaction.prune", () => {
|
|||
const session = await Session.create({})
|
||||
const a = await user(session.id, "first")
|
||||
const b = await assistant(session.id, a.id, tmp.path)
|
||||
await tool(session.id, b.id, "bash", "x".repeat(200_000))
|
||||
await tool(session.id, b.id, "shell", "x".repeat(200_000))
|
||||
await user(session.id, "second")
|
||||
await user(session.id, "third")
|
||||
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ describe("session.llm.hasToolCalls", () => {
|
|||
{
|
||||
type: "tool-call",
|
||||
toolCallId: "call-123",
|
||||
toolName: "bash",
|
||||
toolName: "shell",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -63,7 +63,7 @@ describe("session.llm.hasToolCalls", () => {
|
|||
{
|
||||
type: "tool-result",
|
||||
toolCallId: "call-123",
|
||||
toolName: "bash",
|
||||
toolName: "shell",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -295,7 +295,7 @@ describe("session.message-v2.toModelMessage", () => {
|
|||
...basePart(assistantID, "a2"),
|
||||
type: "tool",
|
||||
callID: "call-1",
|
||||
tool: "bash",
|
||||
tool: "shell",
|
||||
state: {
|
||||
status: "completed",
|
||||
input: { cmd: "ls" },
|
||||
|
|
@ -331,7 +331,7 @@ describe("session.message-v2.toModelMessage", () => {
|
|||
{
|
||||
type: "tool-call",
|
||||
toolCallId: "call-1",
|
||||
toolName: "bash",
|
||||
toolName: "shell",
|
||||
input: { cmd: "ls" },
|
||||
providerExecuted: undefined,
|
||||
providerOptions: { openai: { tool: "meta" } },
|
||||
|
|
@ -344,7 +344,7 @@ describe("session.message-v2.toModelMessage", () => {
|
|||
{
|
||||
type: "tool-result",
|
||||
toolCallId: "call-1",
|
||||
toolName: "bash",
|
||||
toolName: "shell",
|
||||
output: {
|
||||
type: "content",
|
||||
value: [
|
||||
|
|
@ -387,7 +387,7 @@ describe("session.message-v2.toModelMessage", () => {
|
|||
...basePart(assistantID, "a2"),
|
||||
type: "tool",
|
||||
callID: "call-1",
|
||||
tool: "bash",
|
||||
tool: "shell",
|
||||
state: {
|
||||
status: "completed",
|
||||
input: { cmd: "ls" },
|
||||
|
|
@ -414,7 +414,7 @@ describe("session.message-v2.toModelMessage", () => {
|
|||
{
|
||||
type: "tool-call",
|
||||
toolCallId: "call-1",
|
||||
toolName: "bash",
|
||||
toolName: "shell",
|
||||
input: { cmd: "ls" },
|
||||
providerExecuted: undefined,
|
||||
},
|
||||
|
|
@ -426,7 +426,7 @@ describe("session.message-v2.toModelMessage", () => {
|
|||
{
|
||||
type: "tool-result",
|
||||
toolCallId: "call-1",
|
||||
toolName: "bash",
|
||||
toolName: "shell",
|
||||
output: { type: "text", value: "ok" },
|
||||
},
|
||||
],
|
||||
|
|
@ -456,7 +456,7 @@ describe("session.message-v2.toModelMessage", () => {
|
|||
...basePart(assistantID, "a1"),
|
||||
type: "tool",
|
||||
callID: "call-1",
|
||||
tool: "bash",
|
||||
tool: "shell",
|
||||
state: {
|
||||
status: "completed",
|
||||
input: { cmd: "ls" },
|
||||
|
|
@ -481,7 +481,7 @@ describe("session.message-v2.toModelMessage", () => {
|
|||
{
|
||||
type: "tool-call",
|
||||
toolCallId: "call-1",
|
||||
toolName: "bash",
|
||||
toolName: "shell",
|
||||
input: { cmd: "ls" },
|
||||
providerExecuted: undefined,
|
||||
},
|
||||
|
|
@ -493,7 +493,7 @@ describe("session.message-v2.toModelMessage", () => {
|
|||
{
|
||||
type: "tool-result",
|
||||
toolCallId: "call-1",
|
||||
toolName: "bash",
|
||||
toolName: "shell",
|
||||
output: { type: "text", value: "[Old tool result content cleared]" },
|
||||
},
|
||||
],
|
||||
|
|
@ -523,7 +523,7 @@ describe("session.message-v2.toModelMessage", () => {
|
|||
...basePart(assistantID, "a1"),
|
||||
type: "tool",
|
||||
callID: "call-1",
|
||||
tool: "bash",
|
||||
tool: "shell",
|
||||
state: {
|
||||
status: "error",
|
||||
input: { cmd: "ls" },
|
||||
|
|
@ -548,7 +548,7 @@ describe("session.message-v2.toModelMessage", () => {
|
|||
{
|
||||
type: "tool-call",
|
||||
toolCallId: "call-1",
|
||||
toolName: "bash",
|
||||
toolName: "shell",
|
||||
input: { cmd: "ls" },
|
||||
providerExecuted: undefined,
|
||||
providerOptions: { openai: { tool: "meta" } },
|
||||
|
|
@ -561,7 +561,7 @@ describe("session.message-v2.toModelMessage", () => {
|
|||
{
|
||||
type: "tool-result",
|
||||
toolCallId: "call-1",
|
||||
toolName: "bash",
|
||||
toolName: "shell",
|
||||
output: { type: "error-text", value: "nope" },
|
||||
providerOptions: { openai: { tool: "meta" } },
|
||||
},
|
||||
|
|
@ -721,7 +721,7 @@ describe("session.message-v2.toModelMessage", () => {
|
|||
...basePart(assistantID, "a1"),
|
||||
type: "tool",
|
||||
callID: "call-pending",
|
||||
tool: "bash",
|
||||
tool: "shell",
|
||||
state: {
|
||||
status: "pending",
|
||||
input: { cmd: "ls" },
|
||||
|
|
@ -756,7 +756,7 @@ describe("session.message-v2.toModelMessage", () => {
|
|||
{
|
||||
type: "tool-call",
|
||||
toolCallId: "call-pending",
|
||||
toolName: "bash",
|
||||
toolName: "shell",
|
||||
input: { cmd: "ls" },
|
||||
providerExecuted: undefined,
|
||||
},
|
||||
|
|
@ -775,7 +775,7 @@ describe("session.message-v2.toModelMessage", () => {
|
|||
{
|
||||
type: "tool-result",
|
||||
toolCallId: "call-pending",
|
||||
toolName: "bash",
|
||||
toolName: "shell",
|
||||
output: { type: "error-text", value: "[Tool execution was interrupted]" },
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -550,7 +550,7 @@ it.live("session.processor effect tests mark pending tools as aborted on cleanup
|
|||
Effect.gen(function* () {
|
||||
const { processors, session, provider } = yield* boot()
|
||||
|
||||
yield* llm.toolHang("bash", { cmd: "pwd" })
|
||||
yield* llm.toolHang("shell", { cmd: "pwd" })
|
||||
|
||||
const chat = yield* session.create({})
|
||||
const parent = yield* user(chat.id, "tool abort")
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ function tool(sessionID: string, messageID: string) {
|
|||
messageID: messageID as any,
|
||||
sessionID: sessionID as any,
|
||||
type: "tool" as const,
|
||||
tool: "bash",
|
||||
tool: "shell",
|
||||
callID: "call-1",
|
||||
state: {
|
||||
status: "completed" as const,
|
||||
|
|
|
|||
|
|
@ -16,12 +16,11 @@ 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 { ShellToolID } from "../../src/tool/shell/id"
|
||||
import { Log } from "../../src/util/log"
|
||||
import { provideTmpdirServer } from "../fixture/fixture"
|
||||
import { testEffect } from "../lib/effect"
|
||||
|
|
@ -192,7 +191,7 @@ it.live("tool execution produces non-empty session diff (snapshot race)", () =>
|
|||
permission: [{ permission: "*", pattern: "*", action: "allow" }],
|
||||
})
|
||||
|
||||
const shell = ShellTool.from(Shell.name(Shell.acceptable()))
|
||||
const shell = ShellToolID.id
|
||||
|
||||
// Use the active shell tool to create a file
|
||||
const command = `echo 'snapshot race test content' > ${path.join(dir, "race-test.txt")}`
|
||||
|
|
|
|||
|
|
@ -2,10 +2,8 @@ import { describe, expect, test } from "bun:test"
|
|||
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 { ShellKind, ShellToolID } from "../../src/tool/shell/id"
|
||||
import { ShellTool } from "../../src/tool/shell/tool"
|
||||
import { ShellRunner } from "../../src/tool/shell/runner"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { Filesystem } from "../../src/util/filesystem"
|
||||
|
|
@ -49,14 +47,14 @@ const shells = (() => {
|
|||
(item, i) => list.findIndex((other) => other.shell.toLowerCase() === item.shell.toLowerCase()) === i,
|
||||
)
|
||||
})()
|
||||
const ps = shells.filter((item) => ShellTool.powershell(item.label))
|
||||
const ps = shells.filter((item) => ShellKind.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<number | string>) => {
|
||||
const tail = args.length ? ` ${args.map(String).join(" ")}` : ""
|
||||
const text = `${bin} -e ${evalarg(code)}${tail}`
|
||||
if (ShellTool.powershell(sh())) return `& ${text}`
|
||||
if (ShellKind.powershell(sh())) return `& ${text}`
|
||||
return text
|
||||
}
|
||||
|
||||
|
|
@ -93,12 +91,10 @@ const withShell = (item: { label: string; shell: string }, fn: () => Promise<voi
|
|||
}
|
||||
}
|
||||
|
||||
const expectedPermission = () => ShellTool.from(sh())
|
||||
|
||||
const tools = { bash: BashTool, pwsh: PwshTool, powershell: PowershellTool } as const
|
||||
const expectedPermission = () => ShellToolID.id
|
||||
|
||||
const getTool = async () => {
|
||||
return await tools[ShellTool.from(sh())].init()
|
||||
return await ShellTool.init()
|
||||
}
|
||||
|
||||
const each = (name: string, fn: (item: { label: string; shell: string }) => Promise<void>) => {
|
||||
|
|
@ -158,7 +154,7 @@ describe("tool.shell", () => {
|
|||
})
|
||||
|
||||
describe("tool.shell permissions", () => {
|
||||
each("asks for bash permission with correct pattern", async () => {
|
||||
each("asks for shell permission with correct pattern", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
|
|
@ -179,7 +175,7 @@ describe("tool.shell permissions", () => {
|
|||
})
|
||||
})
|
||||
|
||||
each("asks for bash permission with multiple commands", async () => {
|
||||
each("asks for shell permission with multiple commands", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
|
|
@ -256,7 +252,7 @@ describe("tool.shell permissions", () => {
|
|||
if (process.platform === "win32") {
|
||||
if (bash) {
|
||||
test(
|
||||
"asks for nested bash command permissions [bash]",
|
||||
"asks for nested shell command permissions [bash]",
|
||||
withShell({ label: "bash", shell: bash }, async () => {
|
||||
await using outerTmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
|
|
@ -863,7 +859,7 @@ describe("tool.shell permissions", () => {
|
|||
})
|
||||
})
|
||||
|
||||
each("does not ask for bash permission when command is cd only", async () => {
|
||||
each("does not ask for shell permission when command is cd only", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
|
|
@ -974,7 +970,7 @@ describe("tool.shell runtime", () => {
|
|||
ShellRunner.run(
|
||||
{
|
||||
shell: item.shell,
|
||||
name: item.label,
|
||||
kind: ShellKind.from(item.label),
|
||||
command: js("setTimeout(()=>{},30000)"),
|
||||
cwd: projectRoot,
|
||||
env: process.env,
|
||||
|
|
@ -1007,7 +1003,7 @@ describe("tool.shell runtime", () => {
|
|||
ctx,
|
||||
)
|
||||
expect(result.output).toContain("222")
|
||||
expect(result.output).toContain(`${sh()} tool terminated command after exceeding timeout`)
|
||||
expect(result.output).toContain("shell tool terminated command after exceeding timeout")
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
@ -1086,7 +1082,7 @@ describe("tool.shell runtime", () => {
|
|||
const result = await ShellRunner.run(
|
||||
{
|
||||
shell: item.shell,
|
||||
name: item.label,
|
||||
kind: ShellKind.from(item.label),
|
||||
command: js(
|
||||
"process.stdout.write(Buffer.from([0xF0,0x9F]));setTimeout(()=>process.stdout.write(Buffer.from([0x98,0x80])),20);setTimeout(()=>process.exit(0),40)",
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1161,7 +1161,7 @@ export type PermissionConfig =
|
|||
glob?: PermissionRuleConfig
|
||||
grep?: PermissionRuleConfig
|
||||
list?: PermissionRuleConfig
|
||||
bash?: PermissionRuleConfig
|
||||
shell?: PermissionRuleConfig
|
||||
task?: PermissionRuleConfig
|
||||
external_directory?: PermissionRuleConfig
|
||||
todowrite?: PermissionActionConfig
|
||||
|
|
|
|||
|
|
@ -10642,7 +10642,7 @@
|
|||
"list": {
|
||||
"$ref": "#/components/schemas/PermissionRuleConfig"
|
||||
},
|
||||
"bash": {
|
||||
"shell": {
|
||||
"$ref": "#/components/schemas/PermissionRuleConfig"
|
||||
},
|
||||
"task": {
|
||||
|
|
|
|||
|
|
@ -271,7 +271,7 @@ export type ToolInfo = {
|
|||
subtitle?: string
|
||||
}
|
||||
|
||||
const SHELL = new Set(["bash", "pwsh", "powershell"])
|
||||
const SHELL = new Set(["shell"])
|
||||
|
||||
function agentTitle(i18n: UiI18n, type?: string) {
|
||||
if (!type) return i18n.t("ui.tool.agent.default")
|
||||
|
|
@ -1822,7 +1822,7 @@ ToolRegistry.register({
|
|||
})
|
||||
|
||||
ToolRegistry.register({
|
||||
name: "bash",
|
||||
name: "shell",
|
||||
render(props) {
|
||||
const i18n = useI18n()
|
||||
const pending = () => props.status === "pending" || props.status === "running"
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ Interactive playground for animating the Shell tool subtitle ("submessage") in t
|
|||
|
||||
### Production component path
|
||||
- Trigger layout: \`packages/ui/src/components/basic-tool.tsx\`
|
||||
- Bash tool subtitle source: \`packages/ui/src/components/message-part.tsx\` (tool: \`bash\`, \`trigger.subtitle\`)
|
||||
- Shell tool subtitle source: \`packages/ui/src/components/message-part.tsx\` (tool: \`shell\`, \`trigger.subtitle\`)
|
||||
|
||||
### What this playground tunes
|
||||
- Width reveal (spring-driven pixel width via \`useSpring\`)
|
||||
|
|
|
|||
|
|
@ -315,8 +315,8 @@ const TOOL_SAMPLES = {
|
|||
title: "Found 2 matches",
|
||||
metadata: {},
|
||||
},
|
||||
bash: {
|
||||
tool: "bash",
|
||||
shell: {
|
||||
tool: "shell",
|
||||
input: { command: "bun test --filter session", description: "Run session tests" },
|
||||
output:
|
||||
"bun test v1.3.11\n\n✓ session-turn.test.tsx (3 tests) 45ms\n✓ message-part.test.tsx (7 tests) 120ms\n\nTest Suites: 2 passed, 2 total\nTests: 10 passed, 10 total\nTime: 0.89s",
|
||||
|
|
@ -1309,7 +1309,7 @@ function Playground() {
|
|||
toolPart(TOOL_SAMPLES.glob),
|
||||
toolPart(TOOL_SAMPLES.grep),
|
||||
toolPart(TOOL_SAMPLES.edit),
|
||||
toolPart(TOOL_SAMPLES.bash),
|
||||
toolPart(TOOL_SAMPLES.shell),
|
||||
textPart(MARKDOWN_SAMPLES.mixed),
|
||||
])
|
||||
}
|
||||
|
|
@ -1332,7 +1332,7 @@ function Playground() {
|
|||
toolPart(TOOL_SAMPLES.glob),
|
||||
toolPart(TOOL_SAMPLES.grep),
|
||||
toolPart(TOOL_SAMPLES.edit),
|
||||
toolPart(TOOL_SAMPLES.bash),
|
||||
toolPart(TOOL_SAMPLES.shell),
|
||||
textPart(MARKDOWN_SAMPLES.blockquote),
|
||||
])
|
||||
addContextGroupTurn()
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ const docs = `### Overview
|
|||
Tool call failure summary styled like a tool trigger.
|
||||
|
||||
### API
|
||||
- Required: \`tool\` (tool id, e.g. apply_patch, bash)
|
||||
- Required: \`tool\` (tool id, e.g. apply_patch, shell)
|
||||
- Required: \`error\` (error string)
|
||||
|
||||
### Behavior
|
||||
|
|
@ -19,8 +19,8 @@ const samples = [
|
|||
"apply_patch verification failed: Failed to find expected lines in /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/session-turn.tsx",
|
||||
},
|
||||
{
|
||||
tool: "bash",
|
||||
error: "bash Command failed: exit code 1: bun test --watch",
|
||||
tool: "shell",
|
||||
error: "shell Command failed: exit code 1: bun test --watch",
|
||||
},
|
||||
{
|
||||
tool: "read",
|
||||
|
|
@ -72,7 +72,7 @@ export default {
|
|||
argTypes: {
|
||||
tool: {
|
||||
control: "select",
|
||||
options: ["apply_patch", "bash", "read", "glob", "grep", "webfetch", "websearch", "codesearch", "question"],
|
||||
options: ["apply_patch", "shell", "read", "glob", "grep", "webfetch", "websearch", "codesearch", "question"],
|
||||
},
|
||||
error: {
|
||||
control: "text",
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ export function ToolErrorCard(props: ToolErrorCardProps) {
|
|||
webfetch: "ui.tool.webfetch",
|
||||
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",
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ import type { Diagnostic } from "vscode-languageserver-types"
|
|||
import styles from "./part.module.css"
|
||||
|
||||
const MIN_DURATION = 2000
|
||||
const SHELL = new Set(["bash", "pwsh", "powershell"])
|
||||
const SHELL = new Set(["shell"])
|
||||
|
||||
export interface PartProps {
|
||||
index: number
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue