fix(acp): enrich permission prompts (#34079)
This commit is contained in:
@@ -1,9 +1,16 @@
|
||||
import type { AgentSideConnection, PermissionOption, RequestPermissionResponse } from "@agentclientprotocol/sdk"
|
||||
import type {
|
||||
AgentSideConnection,
|
||||
PermissionOption,
|
||||
RequestPermissionResponse,
|
||||
ToolCallContent,
|
||||
ToolCallLocation,
|
||||
ToolCallUpdate,
|
||||
} from "@agentclientprotocol/sdk"
|
||||
import type { Event, OpencodeClient } from "@opencode-ai/sdk/v2"
|
||||
import { applyPatch } from "diff"
|
||||
import { exists, readText } from "@/util/filesystem"
|
||||
import type { ACPSession } from "./session"
|
||||
import { toLocations, toToolKind, type ToolInput } from "./tool"
|
||||
import { pendingToolCall, toLocations, type ToolInput } from "./tool"
|
||||
import { Effect } from "effect"
|
||||
|
||||
type PermissionEvent = Extract<Event, { type: "permission.asked" }>
|
||||
@@ -54,14 +61,11 @@ export class Handler {
|
||||
const result = await this.input.connection
|
||||
.requestPermission({
|
||||
sessionId: permission.sessionID,
|
||||
toolCall: {
|
||||
toolCall: await permissionToolCall({
|
||||
toolCallId: permission.tool?.callID ?? permission.id,
|
||||
status: "pending",
|
||||
title: permission.permission,
|
||||
rawInput: permission.metadata,
|
||||
kind: toToolKind(permission.permission),
|
||||
locations: toLocations(permission.permission, permission.metadata),
|
||||
},
|
||||
toolName: permission.permission,
|
||||
input: permission.metadata,
|
||||
}),
|
||||
options: permissionOptions,
|
||||
})
|
||||
.catch(async () => {
|
||||
@@ -111,6 +115,107 @@ export class Handler {
|
||||
}
|
||||
}
|
||||
|
||||
async function permissionToolCall(input: {
|
||||
readonly toolCallId: string
|
||||
readonly toolName: string
|
||||
readonly input: ToolInput
|
||||
}): Promise<ToolCallUpdate> {
|
||||
const toolCall = pendingToolCall({
|
||||
toolCallId: input.toolCallId,
|
||||
toolName: input.toolName,
|
||||
state: {
|
||||
input: input.input,
|
||||
title: permissionTitle(input.toolName, input.input),
|
||||
},
|
||||
})
|
||||
const content = await permissionContent(input.toolName, input.input)
|
||||
return {
|
||||
...toolCall,
|
||||
locations: permissionLocations(input.toolName, input.input),
|
||||
...(content.length ? { content } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
function permissionTitle(toolName: string, input: ToolInput) {
|
||||
const tool = toolName.toLocaleLowerCase()
|
||||
switch (tool) {
|
||||
case "external_directory":
|
||||
return stringValue(input.description) ?? stringValue(input.command) ?? stringValue(input.parentDir)
|
||||
|
||||
case "webfetch":
|
||||
return stringValue(input.url)
|
||||
|
||||
case "websearch":
|
||||
return stringValue(input.query)
|
||||
|
||||
case "grep":
|
||||
case "glob":
|
||||
return stringValue(input.pattern)
|
||||
|
||||
case "read":
|
||||
case "edit":
|
||||
case "write":
|
||||
return editTitle(input)
|
||||
|
||||
default:
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
function editTitle(input: ToolInput) {
|
||||
const files = fileMetadata(input)
|
||||
if (files.length === 1) return files[0]?.relativePath ?? files[0]?.filePath
|
||||
if (files.length > 1) return `${files.length} files`
|
||||
return stringValue(input.filePath) ?? stringValue(input.filepath) ?? stringValue(input.path)
|
||||
}
|
||||
|
||||
function permissionLocations(toolName: string, input: ToolInput): ToolCallLocation[] {
|
||||
const files = fileMetadata(input)
|
||||
if (files.length) {
|
||||
return Array.from(
|
||||
new Set(files.flatMap((file) => [file.filePath, file.movePath].filter((path): path is string => !!path))),
|
||||
(path) => ({ path }),
|
||||
)
|
||||
}
|
||||
return toLocations(toolName, input)
|
||||
}
|
||||
|
||||
async function permissionContent(toolName: string, input: ToolInput): Promise<ToolCallContent[]> {
|
||||
if (toolName.toLocaleLowerCase() !== "edit") return []
|
||||
|
||||
const files = fileMetadata(input)
|
||||
if (files.length) return diffContentForFiles(files)
|
||||
|
||||
const filepath = stringValue(input.filepath) ?? stringValue(input.filePath)
|
||||
const diff = stringValue(input.diff)
|
||||
if (!filepath || !diff) return []
|
||||
const content = await diffContentForPatch(filepath, diff)
|
||||
return content ? [content] : []
|
||||
}
|
||||
|
||||
async function diffContentForFiles(files: PermissionFileMetadata[]) {
|
||||
const content = await Promise.all(
|
||||
files.map(async (file) => {
|
||||
if (!file.patch) return []
|
||||
const content = await diffContentForPatch(file.filePath, file.patch, file.movePath)
|
||||
return content ? [content] : []
|
||||
}),
|
||||
)
|
||||
return content.flat()
|
||||
}
|
||||
|
||||
async function diffContentForPatch(filepath: string, diff: string, displayPath = filepath) {
|
||||
const content = (await exists(filepath)) ? await readText(filepath) : ""
|
||||
const next = applyPatch(content, diff)
|
||||
if (next === false) return undefined
|
||||
return {
|
||||
type: "diff" as const,
|
||||
path: displayPath,
|
||||
oldText: content,
|
||||
newText: next,
|
||||
}
|
||||
}
|
||||
|
||||
function selectedReply(result: RequestPermissionResponse): Reply {
|
||||
if (result.outcome.outcome !== "selected") return "reject"
|
||||
if (result.outcome.optionId === "once" || result.outcome.optionId === "always") return result.outcome.optionId
|
||||
@@ -121,4 +226,29 @@ function stringValue(value: unknown) {
|
||||
return typeof value === "string" ? value : undefined
|
||||
}
|
||||
|
||||
type PermissionFileMetadata = {
|
||||
readonly filePath: string
|
||||
readonly relativePath?: string
|
||||
readonly movePath?: string
|
||||
readonly patch?: string
|
||||
}
|
||||
|
||||
function fileMetadata(input: ToolInput): PermissionFileMetadata[] {
|
||||
if (!Array.isArray(input.files)) return []
|
||||
return input.files.flatMap((file): PermissionFileMetadata[] => {
|
||||
if (!file || typeof file !== "object") return []
|
||||
const info = file as Record<string, unknown>
|
||||
const filePath = stringValue(info.filePath)
|
||||
if (!filePath) return []
|
||||
return [
|
||||
{
|
||||
filePath,
|
||||
relativePath: stringValue(info.relativePath),
|
||||
movePath: stringValue(info.movePath),
|
||||
patch: stringValue(info.patch),
|
||||
},
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
export * as ACPPermission from "./permission"
|
||||
|
||||
Reference in New Issue
Block a user