Skip to content
Source
Sign inSign up

toolCallDiff.ts

77 lines3.5 KB

1import type { OrchestrationV2TurnItem } from "@t3tools/contracts";
2import { getRenderablePatch, resolveFileDiffPath } from "./diffRendering";
3​
4type FileChange = Extract<OrchestrationV2TurnItem, { type: "file_change" }>;
5​
6function toolCallPatch(item: Pick<FileChange, "diffStr" | "fileName" | "changes">) {
7 let patch = item.diffStr;
8 if (!patch) return patch;
9 const change = item.changes?.[0];
10 const rawFileOperation = change?.operation === "add" || change?.operation === "delete";
11 if (patch.startsWith("diff --git ") || patch.startsWith("--- ")) {
12 if (!rawFileOperation) return patch;
13 const parsed = getRenderablePatch(patch, "tool-call-format");
14 if (
15 parsed?.kind === "files" &&
16 parsed.files.every((file) => {
17 const path = resolveFileDiffPath(file).replaceAll("\\", "/");
18 return (
19 file.hunks.length > 0 &&
20 item.changes?.some((recorded) => {
21 const recordedPath = recorded.path.replaceAll("\\", "/");
22 return (
23 (recorded.operation === "add"
24 ? file.deletionLines.length === 0
25 : recorded.operation === "delete"
26 ? file.additionLines.length === 0
27 : true) &&
28 (recordedPath === path || recordedPath.endsWith(`/${path}`))
29 );
30 })
31 );
32 })
33 )
34 return patch;
35 }
36 // Codex can record added/deleted file contents or bare hunks instead of a complete patch.
37 if (rawFileOperation || !patch.trimStart().startsWith("@@ ")) {
38 if (!rawFileOperation) return patch;
39 const lines = patch.split("\n");
40 const hasFinalNewline = patch.endsWith("\n");
41 if (hasFinalNewline) lines.pop();
42 const added = change.operation === "add";
43 const range = `1,${lines.length}`;
44 patch = `@@ -${added ? "0,0" : range} +${added ? range : "0,0"} @@\n${lines.map((line) => `${added ? "+" : "-"}${line}`).join("\n")}\n${hasFinalNewline ? "" : "\\ No newline at end of file\n"}`;
45 }
46 const oldPath = change?.oldPath ?? item.fileName;
47 const newPath = change?.path ?? item.fileName;
48 const before = change?.operation === "add" ? "/dev/null" : JSON.stringify(`a/${oldPath}`);
49 const after = change?.operation === "delete" ? "/dev/null" : JSON.stringify(`b/${newPath}`);
50 return `diff --git ${JSON.stringify(`a/${oldPath}`)} ${JSON.stringify(`b/${newPath}`)}\n--- ${before}\n+++ ${after}\n${patch.trimStart()}`;
52​
53function recordedText(value: string | undefined) {
54 const suffix = "\n… output truncated for transport";
55 const truncated = value !== undefined && value.endsWith(suffix);
56 return { text: truncated ? value.slice(0, -suffix.length) : value, truncated };
58​
59export function toolCallDiff(
60 item: Pick<FileChange, "diffStr" | "fileName" | "changes" | "oldStr" | "newStr">,
61) {
62 const diff = recordedText(item.diffStr);
63 const operation = item.changes?.[0]?.operation;
64 const emptyContent = diff.text === "" ? "" : undefined;
65 const before = recordedText(item.oldStr ?? (operation === "delete" ? emptyContent : undefined));
66 const after = recordedText(item.newStr ?? (operation === "add" ? emptyContent : undefined));
67 return {
68 patch: toolCallPatch({ ...item, ...(diff.text === undefined ? {} : { diffStr: diff.text }) }),
69 content: diff.text?.trim()
70 ? []
71 : [
72 { label: "Recorded content before", text: before.text },
73 { label: "Recorded content after", text: after.text },
74 ].filter((entry): entry is { label: string; text: string } => entry.text !== undefined),
75 truncated: diff.truncated || before.truncated || after.truncated,
76 };