refactor: rename apply_patch tool
This commit is contained in:
parent
910e37f6d8
commit
5db320b02d
22 changed files with 48 additions and 46 deletions
|
|
@ -20,7 +20,7 @@ const profiles = [
|
|||
{ name: "edit", tool: "edit", input: { filePath: "src/edit.ts" } },
|
||||
{
|
||||
name: "multi patch",
|
||||
tool: "apply_patch",
|
||||
tool: "patch",
|
||||
input: { files: ["src/a.ts", "src/b.ts", "src/old.ts", "src/moved.ts"] },
|
||||
},
|
||||
] as const
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ test("adds patch files incrementally without resetting outer expansion", async (
|
|||
userMessage(),
|
||||
assistantMessage(
|
||||
[
|
||||
toolPart(patchID, "apply_patch", "running", { files: [first.filePath] }, { metadata: { files: [first] } }),
|
||||
toolPart(patchID, "patch", "running", { files: [first.filePath] }, { metadata: { files: [first] } }),
|
||||
textPart(followingID, "Following incremental patch"),
|
||||
],
|
||||
{ completed: false },
|
||||
|
|
@ -49,7 +49,7 @@ test("adds patch files incrementally without resetting outer expansion", async (
|
|||
partUpdated(
|
||||
toolPart(
|
||||
patchID,
|
||||
"apply_patch",
|
||||
"patch",
|
||||
"running",
|
||||
{ files: [first.filePath, second.filePath] },
|
||||
{ metadata: { files: [first, second] } },
|
||||
|
|
@ -61,7 +61,7 @@ test("adds patch files incrementally without resetting outer expansion", async (
|
|||
partUpdated(
|
||||
toolPart(
|
||||
patchID,
|
||||
"apply_patch",
|
||||
"patch",
|
||||
"completed",
|
||||
{ files: [first.filePath, second.filePath, third.filePath] },
|
||||
{ metadata: { files: [first, second, third] } },
|
||||
|
|
|
|||
|
|
@ -295,7 +295,7 @@ function performanceTurn(index: number) {
|
|||
messageID: assistantID,
|
||||
type: "tool",
|
||||
callID: `call_0000_${suffix}_patch`,
|
||||
tool: "apply_patch",
|
||||
tool: "patch",
|
||||
state: {
|
||||
status: "completed",
|
||||
input: { patchText: realisticPatch(index) },
|
||||
|
|
|
|||
|
|
@ -131,7 +131,7 @@ function toolPart(
|
|||
): MessagePart {
|
||||
const metadata =
|
||||
metadataOverride ??
|
||||
(tool === "apply_patch"
|
||||
(tool === "patch"
|
||||
? { files: [patchFile(index, "update"), patchFile(index + 1, index % 2 === 0 ? "add" : "delete")] }
|
||||
: tool === "edit" || tool === "write"
|
||||
? {
|
||||
|
|
@ -219,7 +219,7 @@ function turn(index: number): Message[] {
|
|||
? [toolPart(index, 7, "write", { filePath: `src/generated/write-${index}.ts`, content: code(index, 28) }, 560)]
|
||||
: []),
|
||||
...(index % 8 === 0
|
||||
? [toolPart(index, 8, "apply_patch", { files: [`src/generated/patch-${index}.ts`] }, 620)]
|
||||
? [toolPart(index, 8, "patch", { files: [`src/generated/patch-${index}.ts`] }, 620)]
|
||||
: []),
|
||||
...(index % 7 === 0
|
||||
? [toolPart(index, 4, "bash", { command: "bun typecheck", description: "Verify generated output" }, 620)]
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ test("renders a completed single-file patch", async ({ page }) => {
|
|||
assistantMessage([
|
||||
toolPart(
|
||||
id,
|
||||
"apply_patch",
|
||||
"patch",
|
||||
"completed",
|
||||
{ files: ["src/a.ts"] },
|
||||
{
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ test("preserves nested patch file state through outer collapse and reopen", asyn
|
|||
assistantMessage([
|
||||
toolPart(
|
||||
patchID,
|
||||
"apply_patch",
|
||||
"patch",
|
||||
"completed",
|
||||
{ files: files.map((file) => file.filePath) },
|
||||
{ metadata: { files } },
|
||||
|
|
|
|||
|
|
@ -246,7 +246,7 @@ function editPart(id: string) {
|
|||
function patchPart(id: string) {
|
||||
return toolPart(
|
||||
id,
|
||||
"apply_patch",
|
||||
"patch",
|
||||
"completed",
|
||||
{ files: ["src/a.ts", "src/b.ts"] },
|
||||
{
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import {
|
|||
} from "../performance/timeline-stability/fixture"
|
||||
|
||||
test("renders every tool error outcome without leaking hidden tools", async ({ page }) => {
|
||||
const ordinary = ["bash", "edit", "write", "apply_patch", "webfetch", "websearch", "task", "skill", "mcp_probe"]
|
||||
const ordinary = ["bash", "edit", "write", "patch", "webfetch", "websearch", "task", "skill", "mcp_probe"]
|
||||
const parts = ordinary.map((tool, index) =>
|
||||
toolPart(`prt_error_${index}`, tool, "error", errorInput(tool), { error: `${tool} failed visibly` }),
|
||||
)
|
||||
|
|
@ -90,7 +90,7 @@ function questionInput() {
|
|||
function errorInput(tool: string) {
|
||||
if (tool === "bash") return { command: "exit 1" }
|
||||
if (["edit", "write"].includes(tool)) return { filePath: "src/error.ts", content: "" }
|
||||
if (tool === "apply_patch") return { files: ["src/error.ts"] }
|
||||
if (tool === "patch") return { files: ["src/error.ts"] }
|
||||
if (tool === "webfetch") return { url: "https://example.com" }
|
||||
if (tool === "websearch") return { query: "failure" }
|
||||
if (tool === "task") return { description: "Fail task", subagent_type: "explore" }
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ function toolPart(
|
|||
outputLength = 160,
|
||||
): MessagePart {
|
||||
const metadata =
|
||||
tool === "apply_patch"
|
||||
tool === "patch"
|
||||
? { files: [patchFile(index, "update"), patchFile(index + 1, index % 2 === 0 ? "add" : "delete")] }
|
||||
: tool === "edit" || tool === "write"
|
||||
? {
|
||||
|
|
@ -199,7 +199,7 @@ function turn(index: number): Message[] {
|
|||
? [toolPart(index, 7, "write", { filePath: `src/generated/write-${index}.ts`, content: code(index, 28) }, 560)]
|
||||
: []),
|
||||
...(index % 8 === 0
|
||||
? [toolPart(index, 8, "apply_patch", { files: [`src/generated/patch-${index}.ts`] }, 620)]
|
||||
? [toolPart(index, 8, "patch", { files: [`src/generated/patch-${index}.ts`] }, 620)]
|
||||
: []),
|
||||
...(index % 7 === 0 ? [toolPart(index, 4, "bash", { command: "bun typecheck" }, 620)] : []),
|
||||
...(index % 10 === 0 ? [toolPart(index, 9, "webfetch", { url: "https://example.com/docs/sample" }, 120)] : []),
|
||||
|
|
|
|||
|
|
@ -1245,7 +1245,7 @@ export function MessageTimeline(props: {
|
|||
const value = row()
|
||||
if (value._tag !== "AssistantPart" || value.group.type !== "part") return false
|
||||
const part = getMsgPart(value.group.ref.messageID, value.group.ref.partID)
|
||||
return part?.type === "tool" && ["edit", "write", "apply_patch"].includes(part.tool)
|
||||
return part?.type === "tool" && ["edit", "write", "patch", "apply_patch"].includes(part.tool)
|
||||
}
|
||||
const [ready, setReady] = createSignal(initialItem.size <= timelineFallbackItemSize || !asyncFile())
|
||||
let contentMeasureFrame: number | undefined
|
||||
|
|
|
|||
|
|
@ -628,11 +628,11 @@ function emitEdit(state: State): void {
|
|||
|
||||
function emitPatch(state: State): void {
|
||||
const file = path.join(process.cwd(), "src", "demo-format.ts")
|
||||
const ref = make(state, "apply_patch", {
|
||||
const ref = make(state, "patch", {
|
||||
patchText: "*** Begin Patch\n*** End Patch",
|
||||
})
|
||||
doneTool(state, ref, {
|
||||
title: "apply_patch",
|
||||
title: "patch",
|
||||
output: "",
|
||||
metadata: {
|
||||
files: [
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ type ToolName =
|
|||
| "bash"
|
||||
| "write"
|
||||
| "edit"
|
||||
| "apply_patch"
|
||||
| "patch"
|
||||
| "batch"
|
||||
| "task"
|
||||
| "todowrite"
|
||||
|
|
@ -1094,7 +1094,7 @@ const TOOL_RULES = {
|
|||
},
|
||||
permission: permEdit,
|
||||
},
|
||||
apply_patch: {
|
||||
patch: {
|
||||
view: {
|
||||
output: false,
|
||||
final: true,
|
||||
|
|
|
|||
|
|
@ -200,6 +200,6 @@ export const node = makeLocationNode({ service: Service, layer, deps: [FSUtil.no
|
|||
// TODO: Publish watcher/file-edit events after V2 watcher integration exists.
|
||||
// TODO: Add snapshots / undo after V2 snapshot design exists.
|
||||
// TODO: Notify LSP and collect diagnostics after V2 LSP runtime exists.
|
||||
// TODO: Design multi-file transactions / rollback if apply_patch needs atomic edits.
|
||||
// TODO: Design multi-file transactions / rollback if patch needs atomic edits.
|
||||
// Until then, edits are sequential and report partial application.
|
||||
// TODO: Define crash recovery and idempotency for side effects between Tool.Called and durable settlement.
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ Registrations are scoped:
|
|||
|
||||
## Permissions
|
||||
|
||||
The registry has no `PermissionV2.Service` dependency and performs no execution authorization. An internal built-in-only operation attaches a permission action solely to preserve whole-tool definition filtering; it is not part of public `Tool.make`. Most tools default to their registered name; `edit`, `write`, and `apply_patch` declare the shared `edit` action.
|
||||
The registry has no `PermissionV2.Service` dependency and performs no execution authorization. An internal built-in-only operation attaches a permission action solely to preserve whole-tool definition filtering; it is not part of public `Tool.make`. Most tools default to their registered name; `edit`, `write`, and `patch` declare the shared `edit` action.
|
||||
|
||||
Definition filtering is catalog visibility, not execution authorization. A call still executes the captured leaf policy if it reaches settlement.
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import { Patch } from "../patch"
|
|||
import { PermissionV2 } from "../permission"
|
||||
import { Tool } from "./tool"
|
||||
|
||||
export const name = "apply_patch"
|
||||
export const name = "patch"
|
||||
|
||||
export const Input = Schema.Struct({
|
||||
patchText: Schema.String.annotate({
|
||||
|
|
@ -91,11 +91,11 @@ export const Plugin = {
|
|||
if (!input.patchText.trim()) return yield* new ToolFailure({ message: "patchText is required" })
|
||||
const hunks = yield* Effect.try({
|
||||
try: () => Patch.parse(input.patchText),
|
||||
catch: (cause) => new ToolFailure({ message: `apply_patch verification failed: ${String(cause)}` }),
|
||||
catch: (cause) => new ToolFailure({ message: `patch verification failed: ${String(cause)}` }),
|
||||
})
|
||||
if (hunks.length === 0) return yield* new ToolFailure({ message: "patch rejected: empty patch" })
|
||||
const move = hunks.find((hunk) => hunk.type === "update" && hunk.movePath !== undefined)
|
||||
if (move) return yield* new ToolFailure({ message: "apply_patch moves are not supported yet" })
|
||||
if (move) return yield* new ToolFailure({ message: "patch moves are not supported yet" })
|
||||
|
||||
const targets: Array<{ readonly hunk: Patch.Hunk; readonly target: LocationMutation.Target }> = []
|
||||
for (const hunk of hunks)
|
||||
|
|
|
|||
|
|
@ -191,10 +191,10 @@ const registryLayer = Layer.effect(
|
|||
const registration = entries.at(-1)?.registration
|
||||
if (registration) registrations.set(name, registration)
|
||||
}
|
||||
// OpenAI/GPT models use apply_patch; every other model uses edit and write.
|
||||
// OpenAI/GPT models use patch; every other model uses edit and write.
|
||||
const usePatch = input.model.provider.toLowerCase() === "openai" || input.model.id.toLowerCase().includes("gpt")
|
||||
for (const [name, registration] of registrations) {
|
||||
const wrongEditTool = name === "apply_patch" ? !usePatch : (name === "edit" || name === "write") && usePatch
|
||||
const wrongEditTool = name === "patch" ? !usePatch : (name === "edit" || name === "write") && usePatch
|
||||
if (
|
||||
wrongEditTool ||
|
||||
(registration.deferred && !Flag.CODEMODE_ENABLED) ||
|
||||
|
|
|
|||
|
|
@ -89,16 +89,16 @@ describe("ToolRegistry", () => {
|
|||
read: make(),
|
||||
edit: make("edit"),
|
||||
write: make("edit"),
|
||||
apply_patch: make("edit"),
|
||||
patch: make("edit"),
|
||||
})
|
||||
const names = (model: ToolRegistry.MaterializeInput["model"]) =>
|
||||
service
|
||||
.materialize({ model })
|
||||
.pipe(Effect.map((materialized) => materialized.definitions.map((tool) => tool.name)))
|
||||
|
||||
expect(yield* names({ id: "gpt-5", provider: "openai" })).toEqual(["read", "apply_patch"])
|
||||
expect(yield* names({ id: "gpt-4o", provider: "opencode" })).toEqual(["read", "apply_patch"])
|
||||
expect(yield* names({ id: "computer-use-preview", provider: "openai" })).toEqual(["read", "apply_patch"])
|
||||
expect(yield* names({ id: "gpt-5", provider: "openai" })).toEqual(["read", "patch"])
|
||||
expect(yield* names({ id: "gpt-4o", provider: "opencode" })).toEqual(["read", "patch"])
|
||||
expect(yield* names({ id: "computer-use-preview", provider: "openai" })).toEqual(["read", "patch"])
|
||||
expect(yield* names({ id: "claude-sonnet-4", provider: "anthropic" })).toEqual(["read", "edit", "write"])
|
||||
}),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ const applyPatchToolNode = makeLocationNode({
|
|||
deps: [ToolRegistry.toolsNode, LocationMutation.node, FileMutation.node, FSUtil.node, PermissionV2.node],
|
||||
})
|
||||
|
||||
const sessionID = SessionV2.ID.make("ses_apply_patch_tool_test")
|
||||
const sessionID = SessionV2.ID.make("ses_patch_tool_test")
|
||||
const assertions: PermissionV2.AssertInput[] = []
|
||||
let denyAction: string | undefined
|
||||
let failRemoveTarget: string | undefined
|
||||
|
|
@ -132,10 +132,10 @@ const withTool = <A, E, R>(directory: string, body: (registry: ToolRegistry.Inte
|
|||
const call = (patchText: string, id = "call-apply-patch") => ({
|
||||
sessionID,
|
||||
...toolIdentity,
|
||||
call: { type: "tool-call" as const, id, name: "apply_patch", input: { patchText } },
|
||||
call: { type: "tool-call" as const, id, name: "patch", input: { patchText } },
|
||||
})
|
||||
|
||||
// apply_patch is only materialized for OpenAI/GPT models.
|
||||
// patch is only materialized for OpenAI/GPT models.
|
||||
const model = { id: "gpt-5", provider: "openai" }
|
||||
|
||||
const exists = (target: string) =>
|
||||
|
|
@ -162,7 +162,7 @@ describe("ApplyPatchTool", () => {
|
|||
withTool(tmp.path, (registry) =>
|
||||
Effect.gen(function* () {
|
||||
expect((yield* toolDefinitions(registry, undefined, model)).map((tool) => tool.name)).toEqual([
|
||||
"apply_patch",
|
||||
"patch",
|
||||
])
|
||||
const settled = yield* settleTool(
|
||||
registry,
|
||||
|
|
@ -241,7 +241,7 @@ describe("ApplyPatchTool", () => {
|
|||
),
|
||||
model,
|
||||
),
|
||||
).toEqual({ type: "error", value: "apply_patch moves are not supported yet" })
|
||||
).toEqual({ type: "error", value: "patch moves are not supported yet" })
|
||||
expect(yield* exists(path.join(tmp.path, "created.txt"))).toBe(false)
|
||||
expect(assertions).toEqual([])
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -536,6 +536,7 @@ export function getToolInfo(
|
|||
title: i18n.t("ui.messagePart.title.write"),
|
||||
subtitle: input.filePath ? getFilename(input.filePath) : undefined,
|
||||
}
|
||||
case "patch":
|
||||
case "apply_patch":
|
||||
return {
|
||||
icon: "code-lines",
|
||||
|
|
@ -728,7 +729,7 @@ export function renderable(part: PartType, showReasoningSummaries = true) {
|
|||
|
||||
function toolDefaultOpen(tool: string, shell = false, edit = false) {
|
||||
if (tool === "bash") return shell
|
||||
if (tool === "edit" || tool === "write" || tool === "apply_patch") return edit
|
||||
if (tool === "edit" || tool === "write" || tool === "patch" || tool === "apply_patch") return edit
|
||||
}
|
||||
|
||||
export function partDefaultOpen(part: PartType, shell = false, edit = false) {
|
||||
|
|
@ -1449,7 +1450,7 @@ export function registerTool(input: { name: string; render?: ToolComponent }) {
|
|||
}
|
||||
|
||||
export function getTool(name: string) {
|
||||
return state[name]?.render
|
||||
return state[name === "apply_patch" ? "patch" : name]?.render
|
||||
}
|
||||
|
||||
export const ToolRegistry = {
|
||||
|
|
@ -2272,7 +2273,7 @@ ToolRegistry.register({
|
|||
})
|
||||
|
||||
ToolRegistry.register({
|
||||
name: "apply_patch",
|
||||
name: "patch",
|
||||
render(props) {
|
||||
const i18n = useI18n()
|
||||
const fileComponent = useFileComponent()
|
||||
|
|
|
|||
|
|
@ -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. patch, bash)
|
||||
- Required: \`error\` (error string)
|
||||
|
||||
### Behavior
|
||||
|
|
@ -14,9 +14,9 @@ Tool call failure summary styled like a tool trigger.
|
|||
|
||||
const samples = [
|
||||
{
|
||||
tool: "apply_patch",
|
||||
tool: "patch",
|
||||
error:
|
||||
"apply_patch verification failed: Failed to find expected lines in /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/session-turn.tsx",
|
||||
"patch verification failed: Failed to find expected lines in /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/session-turn.tsx",
|
||||
},
|
||||
{
|
||||
tool: "bash",
|
||||
|
|
@ -62,13 +62,13 @@ export default {
|
|||
},
|
||||
},
|
||||
args: {
|
||||
tool: "apply_patch",
|
||||
tool: "patch",
|
||||
error: samples[0].error,
|
||||
},
|
||||
argTypes: {
|
||||
tool: {
|
||||
control: "select",
|
||||
options: ["apply_patch", "bash", "read", "glob", "grep", "webfetch", "websearch", "question"],
|
||||
options: ["patch", "bash", "read", "glob", "grep", "webfetch", "websearch", "question"],
|
||||
},
|
||||
error: {
|
||||
control: "text",
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ export function ToolErrorCard(props: ToolErrorCardProps) {
|
|||
webfetch: "ui.tool.webfetch",
|
||||
websearch: "ui.tool.websearch",
|
||||
bash: "ui.tool.shell",
|
||||
patch: "ui.tool.patch",
|
||||
apply_patch: "ui.tool.patch",
|
||||
question: "ui.tool.questions",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1908,7 +1908,7 @@ function ToolPart(props: { part: SessionMessageAssistantTool }) {
|
|||
<Match when={display() === "execute"}>
|
||||
<Execute {...toolprops} />
|
||||
</Match>
|
||||
<Match when={display() === "apply_patch"}>
|
||||
<Match when={display() === "patch"}>
|
||||
<ApplyPatch {...toolprops} />
|
||||
</Match>
|
||||
<Match when={display() === "todowrite"}>
|
||||
|
|
@ -2737,7 +2737,7 @@ const toolDisplays = new Set([
|
|||
"edit",
|
||||
"subagent",
|
||||
"execute",
|
||||
"apply_patch",
|
||||
"patch",
|
||||
"todowrite",
|
||||
"question",
|
||||
"skill",
|
||||
|
|
@ -2746,7 +2746,7 @@ const toolDisplays = new Set([
|
|||
export function toolDisplay(tool: string) {
|
||||
// Legacy transcripts recorded the shell tool as "bash" and the subagent tool as "task"; render
|
||||
// them with the renamed views.
|
||||
const normalized = tool === "bash" ? "shell" : tool === "task" ? "subagent" : tool
|
||||
const normalized = tool === "bash" ? "shell" : tool === "task" ? "subagent" : tool === "apply_patch" ? "patch" : tool
|
||||
return toolDisplays.has(normalized) ? normalized : "generic"
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue