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: "edit", tool: "edit", input: { filePath: "src/edit.ts" } },
|
||||||
{
|
{
|
||||||
name: "multi patch",
|
name: "multi patch",
|
||||||
tool: "apply_patch",
|
tool: "patch",
|
||||||
input: { files: ["src/a.ts", "src/b.ts", "src/old.ts", "src/moved.ts"] },
|
input: { files: ["src/a.ts", "src/b.ts", "src/old.ts", "src/moved.ts"] },
|
||||||
},
|
},
|
||||||
] as const
|
] as const
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ test("adds patch files incrementally without resetting outer expansion", async (
|
||||||
userMessage(),
|
userMessage(),
|
||||||
assistantMessage(
|
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"),
|
textPart(followingID, "Following incremental patch"),
|
||||||
],
|
],
|
||||||
{ completed: false },
|
{ completed: false },
|
||||||
|
|
@ -49,7 +49,7 @@ test("adds patch files incrementally without resetting outer expansion", async (
|
||||||
partUpdated(
|
partUpdated(
|
||||||
toolPart(
|
toolPart(
|
||||||
patchID,
|
patchID,
|
||||||
"apply_patch",
|
"patch",
|
||||||
"running",
|
"running",
|
||||||
{ files: [first.filePath, second.filePath] },
|
{ files: [first.filePath, second.filePath] },
|
||||||
{ metadata: { files: [first, second] } },
|
{ metadata: { files: [first, second] } },
|
||||||
|
|
@ -61,7 +61,7 @@ test("adds patch files incrementally without resetting outer expansion", async (
|
||||||
partUpdated(
|
partUpdated(
|
||||||
toolPart(
|
toolPart(
|
||||||
patchID,
|
patchID,
|
||||||
"apply_patch",
|
"patch",
|
||||||
"completed",
|
"completed",
|
||||||
{ files: [first.filePath, second.filePath, third.filePath] },
|
{ files: [first.filePath, second.filePath, third.filePath] },
|
||||||
{ metadata: { files: [first, second, third] } },
|
{ metadata: { files: [first, second, third] } },
|
||||||
|
|
|
||||||
|
|
@ -295,7 +295,7 @@ function performanceTurn(index: number) {
|
||||||
messageID: assistantID,
|
messageID: assistantID,
|
||||||
type: "tool",
|
type: "tool",
|
||||||
callID: `call_0000_${suffix}_patch`,
|
callID: `call_0000_${suffix}_patch`,
|
||||||
tool: "apply_patch",
|
tool: "patch",
|
||||||
state: {
|
state: {
|
||||||
status: "completed",
|
status: "completed",
|
||||||
input: { patchText: realisticPatch(index) },
|
input: { patchText: realisticPatch(index) },
|
||||||
|
|
|
||||||
|
|
@ -131,7 +131,7 @@ function toolPart(
|
||||||
): MessagePart {
|
): MessagePart {
|
||||||
const metadata =
|
const metadata =
|
||||||
metadataOverride ??
|
metadataOverride ??
|
||||||
(tool === "apply_patch"
|
(tool === "patch"
|
||||||
? { files: [patchFile(index, "update"), patchFile(index + 1, index % 2 === 0 ? "add" : "delete")] }
|
? { files: [patchFile(index, "update"), patchFile(index + 1, index % 2 === 0 ? "add" : "delete")] }
|
||||||
: tool === "edit" || tool === "write"
|
: 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)]
|
? [toolPart(index, 7, "write", { filePath: `src/generated/write-${index}.ts`, content: code(index, 28) }, 560)]
|
||||||
: []),
|
: []),
|
||||||
...(index % 8 === 0
|
...(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
|
...(index % 7 === 0
|
||||||
? [toolPart(index, 4, "bash", { command: "bun typecheck", description: "Verify generated output" }, 620)]
|
? [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([
|
assistantMessage([
|
||||||
toolPart(
|
toolPart(
|
||||||
id,
|
id,
|
||||||
"apply_patch",
|
"patch",
|
||||||
"completed",
|
"completed",
|
||||||
{ files: ["src/a.ts"] },
|
{ files: ["src/a.ts"] },
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ test("preserves nested patch file state through outer collapse and reopen", asyn
|
||||||
assistantMessage([
|
assistantMessage([
|
||||||
toolPart(
|
toolPart(
|
||||||
patchID,
|
patchID,
|
||||||
"apply_patch",
|
"patch",
|
||||||
"completed",
|
"completed",
|
||||||
{ files: files.map((file) => file.filePath) },
|
{ files: files.map((file) => file.filePath) },
|
||||||
{ metadata: { files } },
|
{ metadata: { files } },
|
||||||
|
|
|
||||||
|
|
@ -246,7 +246,7 @@ function editPart(id: string) {
|
||||||
function patchPart(id: string) {
|
function patchPart(id: string) {
|
||||||
return toolPart(
|
return toolPart(
|
||||||
id,
|
id,
|
||||||
"apply_patch",
|
"patch",
|
||||||
"completed",
|
"completed",
|
||||||
{ files: ["src/a.ts", "src/b.ts"] },
|
{ files: ["src/a.ts", "src/b.ts"] },
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import {
|
||||||
} from "../performance/timeline-stability/fixture"
|
} from "../performance/timeline-stability/fixture"
|
||||||
|
|
||||||
test("renders every tool error outcome without leaking hidden tools", async ({ page }) => {
|
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) =>
|
const parts = ordinary.map((tool, index) =>
|
||||||
toolPart(`prt_error_${index}`, tool, "error", errorInput(tool), { error: `${tool} failed visibly` }),
|
toolPart(`prt_error_${index}`, tool, "error", errorInput(tool), { error: `${tool} failed visibly` }),
|
||||||
)
|
)
|
||||||
|
|
@ -90,7 +90,7 @@ function questionInput() {
|
||||||
function errorInput(tool: string) {
|
function errorInput(tool: string) {
|
||||||
if (tool === "bash") return { command: "exit 1" }
|
if (tool === "bash") return { command: "exit 1" }
|
||||||
if (["edit", "write"].includes(tool)) return { filePath: "src/error.ts", content: "" }
|
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 === "webfetch") return { url: "https://example.com" }
|
||||||
if (tool === "websearch") return { query: "failure" }
|
if (tool === "websearch") return { query: "failure" }
|
||||||
if (tool === "task") return { description: "Fail task", subagent_type: "explore" }
|
if (tool === "task") return { description: "Fail task", subagent_type: "explore" }
|
||||||
|
|
|
||||||
|
|
@ -120,7 +120,7 @@ function toolPart(
|
||||||
outputLength = 160,
|
outputLength = 160,
|
||||||
): MessagePart {
|
): MessagePart {
|
||||||
const metadata =
|
const metadata =
|
||||||
tool === "apply_patch"
|
tool === "patch"
|
||||||
? { files: [patchFile(index, "update"), patchFile(index + 1, index % 2 === 0 ? "add" : "delete")] }
|
? { files: [patchFile(index, "update"), patchFile(index + 1, index % 2 === 0 ? "add" : "delete")] }
|
||||||
: tool === "edit" || tool === "write"
|
: 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)]
|
? [toolPart(index, 7, "write", { filePath: `src/generated/write-${index}.ts`, content: code(index, 28) }, 560)]
|
||||||
: []),
|
: []),
|
||||||
...(index % 8 === 0
|
...(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 % 7 === 0 ? [toolPart(index, 4, "bash", { command: "bun typecheck" }, 620)] : []),
|
||||||
...(index % 10 === 0 ? [toolPart(index, 9, "webfetch", { url: "https://example.com/docs/sample" }, 120)] : []),
|
...(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()
|
const value = row()
|
||||||
if (value._tag !== "AssistantPart" || value.group.type !== "part") return false
|
if (value._tag !== "AssistantPart" || value.group.type !== "part") return false
|
||||||
const part = getMsgPart(value.group.ref.messageID, value.group.ref.partID)
|
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())
|
const [ready, setReady] = createSignal(initialItem.size <= timelineFallbackItemSize || !asyncFile())
|
||||||
let contentMeasureFrame: number | undefined
|
let contentMeasureFrame: number | undefined
|
||||||
|
|
|
||||||
|
|
@ -628,11 +628,11 @@ function emitEdit(state: State): void {
|
||||||
|
|
||||||
function emitPatch(state: State): void {
|
function emitPatch(state: State): void {
|
||||||
const file = path.join(process.cwd(), "src", "demo-format.ts")
|
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",
|
patchText: "*** Begin Patch\n*** End Patch",
|
||||||
})
|
})
|
||||||
doneTool(state, ref, {
|
doneTool(state, ref, {
|
||||||
title: "apply_patch",
|
title: "patch",
|
||||||
output: "",
|
output: "",
|
||||||
metadata: {
|
metadata: {
|
||||||
files: [
|
files: [
|
||||||
|
|
|
||||||
|
|
@ -119,7 +119,7 @@ type ToolName =
|
||||||
| "bash"
|
| "bash"
|
||||||
| "write"
|
| "write"
|
||||||
| "edit"
|
| "edit"
|
||||||
| "apply_patch"
|
| "patch"
|
||||||
| "batch"
|
| "batch"
|
||||||
| "task"
|
| "task"
|
||||||
| "todowrite"
|
| "todowrite"
|
||||||
|
|
@ -1094,7 +1094,7 @@ const TOOL_RULES = {
|
||||||
},
|
},
|
||||||
permission: permEdit,
|
permission: permEdit,
|
||||||
},
|
},
|
||||||
apply_patch: {
|
patch: {
|
||||||
view: {
|
view: {
|
||||||
output: false,
|
output: false,
|
||||||
final: true,
|
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: Publish watcher/file-edit events after V2 watcher integration exists.
|
||||||
// TODO: Add snapshots / undo after V2 snapshot design exists.
|
// TODO: Add snapshots / undo after V2 snapshot design exists.
|
||||||
// TODO: Notify LSP and collect diagnostics after V2 LSP runtime 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.
|
// Until then, edits are sequential and report partial application.
|
||||||
// TODO: Define crash recovery and idempotency for side effects between Tool.Called and durable settlement.
|
// TODO: Define crash recovery and idempotency for side effects between Tool.Called and durable settlement.
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ Registrations are scoped:
|
||||||
|
|
||||||
## Permissions
|
## 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.
|
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 { PermissionV2 } from "../permission"
|
||||||
import { Tool } from "./tool"
|
import { Tool } from "./tool"
|
||||||
|
|
||||||
export const name = "apply_patch"
|
export const name = "patch"
|
||||||
|
|
||||||
export const Input = Schema.Struct({
|
export const Input = Schema.Struct({
|
||||||
patchText: Schema.String.annotate({
|
patchText: Schema.String.annotate({
|
||||||
|
|
@ -91,11 +91,11 @@ export const Plugin = {
|
||||||
if (!input.patchText.trim()) return yield* new ToolFailure({ message: "patchText is required" })
|
if (!input.patchText.trim()) return yield* new ToolFailure({ message: "patchText is required" })
|
||||||
const hunks = yield* Effect.try({
|
const hunks = yield* Effect.try({
|
||||||
try: () => Patch.parse(input.patchText),
|
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" })
|
if (hunks.length === 0) return yield* new ToolFailure({ message: "patch rejected: empty patch" })
|
||||||
const move = hunks.find((hunk) => hunk.type === "update" && hunk.movePath !== undefined)
|
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 }> = []
|
const targets: Array<{ readonly hunk: Patch.Hunk; readonly target: LocationMutation.Target }> = []
|
||||||
for (const hunk of hunks)
|
for (const hunk of hunks)
|
||||||
|
|
|
||||||
|
|
@ -191,10 +191,10 @@ const registryLayer = Layer.effect(
|
||||||
const registration = entries.at(-1)?.registration
|
const registration = entries.at(-1)?.registration
|
||||||
if (registration) registrations.set(name, 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")
|
const usePatch = input.model.provider.toLowerCase() === "openai" || input.model.id.toLowerCase().includes("gpt")
|
||||||
for (const [name, registration] of registrations) {
|
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 (
|
if (
|
||||||
wrongEditTool ||
|
wrongEditTool ||
|
||||||
(registration.deferred && !Flag.CODEMODE_ENABLED) ||
|
(registration.deferred && !Flag.CODEMODE_ENABLED) ||
|
||||||
|
|
|
||||||
|
|
@ -89,16 +89,16 @@ describe("ToolRegistry", () => {
|
||||||
read: make(),
|
read: make(),
|
||||||
edit: make("edit"),
|
edit: make("edit"),
|
||||||
write: make("edit"),
|
write: make("edit"),
|
||||||
apply_patch: make("edit"),
|
patch: make("edit"),
|
||||||
})
|
})
|
||||||
const names = (model: ToolRegistry.MaterializeInput["model"]) =>
|
const names = (model: ToolRegistry.MaterializeInput["model"]) =>
|
||||||
service
|
service
|
||||||
.materialize({ model })
|
.materialize({ model })
|
||||||
.pipe(Effect.map((materialized) => materialized.definitions.map((tool) => tool.name)))
|
.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-5", provider: "openai" })).toEqual(["read", "patch"])
|
||||||
expect(yield* names({ id: "gpt-4o", provider: "opencode" })).toEqual(["read", "apply_patch"])
|
expect(yield* names({ id: "gpt-4o", provider: "opencode" })).toEqual(["read", "patch"])
|
||||||
expect(yield* names({ id: "computer-use-preview", provider: "openai" })).toEqual(["read", "apply_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"])
|
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],
|
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[] = []
|
const assertions: PermissionV2.AssertInput[] = []
|
||||||
let denyAction: string | undefined
|
let denyAction: string | undefined
|
||||||
let failRemoveTarget: 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") => ({
|
const call = (patchText: string, id = "call-apply-patch") => ({
|
||||||
sessionID,
|
sessionID,
|
||||||
...toolIdentity,
|
...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 model = { id: "gpt-5", provider: "openai" }
|
||||||
|
|
||||||
const exists = (target: string) =>
|
const exists = (target: string) =>
|
||||||
|
|
@ -162,7 +162,7 @@ describe("ApplyPatchTool", () => {
|
||||||
withTool(tmp.path, (registry) =>
|
withTool(tmp.path, (registry) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
expect((yield* toolDefinitions(registry, undefined, model)).map((tool) => tool.name)).toEqual([
|
expect((yield* toolDefinitions(registry, undefined, model)).map((tool) => tool.name)).toEqual([
|
||||||
"apply_patch",
|
"patch",
|
||||||
])
|
])
|
||||||
const settled = yield* settleTool(
|
const settled = yield* settleTool(
|
||||||
registry,
|
registry,
|
||||||
|
|
@ -241,7 +241,7 @@ describe("ApplyPatchTool", () => {
|
||||||
),
|
),
|
||||||
model,
|
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(yield* exists(path.join(tmp.path, "created.txt"))).toBe(false)
|
||||||
expect(assertions).toEqual([])
|
expect(assertions).toEqual([])
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -536,6 +536,7 @@ export function getToolInfo(
|
||||||
title: i18n.t("ui.messagePart.title.write"),
|
title: i18n.t("ui.messagePart.title.write"),
|
||||||
subtitle: input.filePath ? getFilename(input.filePath) : undefined,
|
subtitle: input.filePath ? getFilename(input.filePath) : undefined,
|
||||||
}
|
}
|
||||||
|
case "patch":
|
||||||
case "apply_patch":
|
case "apply_patch":
|
||||||
return {
|
return {
|
||||||
icon: "code-lines",
|
icon: "code-lines",
|
||||||
|
|
@ -728,7 +729,7 @@ export function renderable(part: PartType, showReasoningSummaries = true) {
|
||||||
|
|
||||||
function toolDefaultOpen(tool: string, shell = false, edit = false) {
|
function toolDefaultOpen(tool: string, shell = false, edit = false) {
|
||||||
if (tool === "bash") return shell
|
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) {
|
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) {
|
export function getTool(name: string) {
|
||||||
return state[name]?.render
|
return state[name === "apply_patch" ? "patch" : name]?.render
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ToolRegistry = {
|
export const ToolRegistry = {
|
||||||
|
|
@ -2272,7 +2273,7 @@ ToolRegistry.register({
|
||||||
})
|
})
|
||||||
|
|
||||||
ToolRegistry.register({
|
ToolRegistry.register({
|
||||||
name: "apply_patch",
|
name: "patch",
|
||||||
render(props) {
|
render(props) {
|
||||||
const i18n = useI18n()
|
const i18n = useI18n()
|
||||||
const fileComponent = useFileComponent()
|
const fileComponent = useFileComponent()
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ const docs = `### Overview
|
||||||
Tool call failure summary styled like a tool trigger.
|
Tool call failure summary styled like a tool trigger.
|
||||||
|
|
||||||
### API
|
### API
|
||||||
- Required: \`tool\` (tool id, e.g. apply_patch, bash)
|
- Required: \`tool\` (tool id, e.g. patch, bash)
|
||||||
- Required: \`error\` (error string)
|
- Required: \`error\` (error string)
|
||||||
|
|
||||||
### Behavior
|
### Behavior
|
||||||
|
|
@ -14,9 +14,9 @@ Tool call failure summary styled like a tool trigger.
|
||||||
|
|
||||||
const samples = [
|
const samples = [
|
||||||
{
|
{
|
||||||
tool: "apply_patch",
|
tool: "patch",
|
||||||
error:
|
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",
|
tool: "bash",
|
||||||
|
|
@ -62,13 +62,13 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
args: {
|
args: {
|
||||||
tool: "apply_patch",
|
tool: "patch",
|
||||||
error: samples[0].error,
|
error: samples[0].error,
|
||||||
},
|
},
|
||||||
argTypes: {
|
argTypes: {
|
||||||
tool: {
|
tool: {
|
||||||
control: "select",
|
control: "select",
|
||||||
options: ["apply_patch", "bash", "read", "glob", "grep", "webfetch", "websearch", "question"],
|
options: ["patch", "bash", "read", "glob", "grep", "webfetch", "websearch", "question"],
|
||||||
},
|
},
|
||||||
error: {
|
error: {
|
||||||
control: "text",
|
control: "text",
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ export function ToolErrorCard(props: ToolErrorCardProps) {
|
||||||
webfetch: "ui.tool.webfetch",
|
webfetch: "ui.tool.webfetch",
|
||||||
websearch: "ui.tool.websearch",
|
websearch: "ui.tool.websearch",
|
||||||
bash: "ui.tool.shell",
|
bash: "ui.tool.shell",
|
||||||
|
patch: "ui.tool.patch",
|
||||||
apply_patch: "ui.tool.patch",
|
apply_patch: "ui.tool.patch",
|
||||||
question: "ui.tool.questions",
|
question: "ui.tool.questions",
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1908,7 +1908,7 @@ function ToolPart(props: { part: SessionMessageAssistantTool }) {
|
||||||
<Match when={display() === "execute"}>
|
<Match when={display() === "execute"}>
|
||||||
<Execute {...toolprops} />
|
<Execute {...toolprops} />
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={display() === "apply_patch"}>
|
<Match when={display() === "patch"}>
|
||||||
<ApplyPatch {...toolprops} />
|
<ApplyPatch {...toolprops} />
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={display() === "todowrite"}>
|
<Match when={display() === "todowrite"}>
|
||||||
|
|
@ -2737,7 +2737,7 @@ const toolDisplays = new Set([
|
||||||
"edit",
|
"edit",
|
||||||
"subagent",
|
"subagent",
|
||||||
"execute",
|
"execute",
|
||||||
"apply_patch",
|
"patch",
|
||||||
"todowrite",
|
"todowrite",
|
||||||
"question",
|
"question",
|
||||||
"skill",
|
"skill",
|
||||||
|
|
@ -2746,7 +2746,7 @@ const toolDisplays = new Set([
|
||||||
export function toolDisplay(tool: string) {
|
export function toolDisplay(tool: string) {
|
||||||
// Legacy transcripts recorded the shell tool as "bash" and the subagent tool as "task"; render
|
// Legacy transcripts recorded the shell tool as "bash" and the subagent tool as "task"; render
|
||||||
// them with the renamed views.
|
// 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"
|
return toolDisplays.has(normalized) ? normalized : "generic"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue