diff --git a/packages/opencode/src/acp/agent.ts b/packages/opencode/src/acp/agent.ts index 8b74b9c9bad3..6313e5acb579 100644 --- a/packages/opencode/src/acp/agent.ts +++ b/packages/opencode/src/acp/agent.ts @@ -147,11 +147,6 @@ export class Agent implements ACPAgent { private shellSnapshots = new Map() private toolStarts = new Set() private permissionQueues = new Map>() - private permissionOptions: PermissionOption[] = [ - { optionId: "once", kind: "allow_once", name: "Allow once" }, - { optionId: "always", kind: "allow_always", name: "Always allow" }, - { optionId: "reject", kind: "reject_once", name: "Reject" }, - ] constructor(connection: AgentSideConnection, config: ACPConfig) { this.connection = connection @@ -205,12 +200,12 @@ export class Agent implements ACPAgent { toolCall: { toolCallId: permission.tool?.callID ?? permission.id, status: "pending", - title: permission.permission, + title: toPermissionTitle(permission.permission, permission.metadata), rawInput: permission.metadata, kind: toToolKind(permission.permission), locations: toLocations(permission.permission, permission.metadata), }, - options: this.permissionOptions, + options: toPermissionOptions(permission.always), }) .catch(async (error) => { log.error("failed to request permission from ACP", { @@ -301,7 +296,7 @@ export class Agent implements ACPAgent { toolCallId: part.callID, status: "in_progress", kind: toToolKind(part.tool), - title: part.tool, + title: (part.state.input as { description?: string })?.description || part.tool, locations: toLocations(part.tool, part.state.input), rawInput: part.state.input, }, @@ -329,7 +324,7 @@ export class Agent implements ACPAgent { toolCallId: part.callID, status: "in_progress", kind: toToolKind(part.tool), - title: part.tool, + title: (part.state.input as { description?: string })?.description || part.tool, locations: toLocations(part.tool, part.state.input), rawInput: part.state.input, ...(content.length > 0 && { content }), @@ -403,7 +398,7 @@ export class Agent implements ACPAgent { toolCallId: part.callID, status: "failed", kind: toToolKind(part.tool), - title: part.tool, + title: (part.state.input as { description?: string })?.description || part.tool, rawInput: part.state.input, content: [ { @@ -814,7 +809,7 @@ export class Agent implements ACPAgent { toolCallId: part.callID, status: "in_progress", kind: toToolKind(part.tool), - title: part.tool, + title: (part.state.input as { description?: string })?.description || part.tool, locations: toLocations(part.tool, part.state.input), rawInput: part.state.input, ...(runningContent.length > 0 && { content: runningContent }), @@ -886,7 +881,7 @@ export class Agent implements ACPAgent { toolCallId: part.callID, status: "failed", kind: toToolKind(part.tool), - title: part.tool, + title: (part.state.input as { description?: string })?.description || part.tool, rawInput: part.state.input, content: [ { @@ -1048,7 +1043,7 @@ export class Agent implements ACPAgent { update: { sessionUpdate: "tool_call", toolCallId: part.callID, - title: part.tool, + title: (part.state.input as { description?: string })?.description || part.tool, kind: toToolKind(part.tool), status: "pending", locations: [], @@ -1579,7 +1574,9 @@ function toLocations(toolName: string, input: Record): { path: stri case "read": case "edit": case "write": - return input["filePath"] ? [{ path: input["filePath"] }] : [] + return input["filePath"] || input["filepath"] + ? [{ path: input["filePath"] ?? input["filepath"] }] + : [] case "glob": case "grep": return input["path"] ? [{ path: input["path"] }] : [] @@ -1594,6 +1591,33 @@ function toLocations(toolName: string, input: Record): { path: stri } } +function toPermissionOptions(always: string[] | undefined): PermissionOption[] { + const alwaysName = + always && always.length > 0 + ? `Always allow: ${always.join(", ")}` + : "Always allow" + return [ + { optionId: "once", kind: "allow_once", name: "Allow once" }, + { optionId: "always", kind: "allow_always", name: alwaysName }, + { optionId: "reject", kind: "reject_once", name: "Reject" }, + ] +} + +function toPermissionTitle(permission: string, metadata: Record): string { + const str = (v: unknown): string | undefined => (typeof v === "string" && v ? v : undefined) + return ( + str(metadata.description) ?? + str(metadata.command) ?? + str(metadata.filepath) ?? + str(metadata.filePath) ?? + str(metadata.pattern) ?? + str(metadata.url) ?? + str(metadata.repository) ?? + str(metadata.path) ?? + permission + ) +} + function completedToolContent(part: ToolPart, kind: ToolKind): ToolCallContent[] { if (part.state.status !== "completed") return [] diff --git a/packages/opencode/src/tool/shell.ts b/packages/opencode/src/tool/shell.ts index 506d98466e76..2a98e2e963e8 100644 --- a/packages/opencode/src/tool/shell.ts +++ b/packages/opencode/src/tool/shell.ts @@ -263,7 +263,12 @@ const parse = Effect.fn("ShellTool.parse")(function* (command: string, ps: boole return tree }) -const ask = Effect.fn("ShellTool.ask")(function* (ctx: Tool.Context, scan: Scan) { +const ask = Effect.fn("ShellTool.ask")(function* ( + ctx: Tool.Context, + scan: Scan, + command: string, + description: string | undefined, +) { if (scan.dirs.size > 0) { const globs = Array.from(scan.dirs).map((dir) => { if (process.platform === "win32") return AppFileSystem.normalizePathPattern(path.join(dir, "*")) @@ -273,7 +278,7 @@ const ask = Effect.fn("ShellTool.ask")(function* (ctx: Tool.Context, scan: Scan) permission: "external_directory", patterns: globs, always: globs, - metadata: {}, + metadata: { command, description }, }) } @@ -282,7 +287,7 @@ const ask = Effect.fn("ShellTool.ask")(function* (ctx: Tool.Context, scan: Scan) permission: ShellID.ToolID, patterns: Array.from(scan.patterns), always: Array.from(scan.always), - metadata: {}, + metadata: { command, description }, }) }) @@ -625,7 +630,7 @@ export const ShellTool = Tool.define( ) const scan = yield* collect(tree.rootNode, cwd, ps, shell, instanceCtx) if (!containsPath(cwd, instanceCtx)) scan.dirs.add(cwd) - yield* ask(ctx, scan) + yield* ask(ctx, scan, params.command, params.description) }), )