Files
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

222 lines
6.3 KiB
TypeScript

import type {
AgentTurnSummaryPatchBatchVM,
AgentTurnSummaryPatchChangeVM
} from "../contracts/agentTurnSummaryRowVM";
import {
isInsideOrEqualWorkspaceFilePath,
normalizeWorkspaceFilePath
} from "../../../actions/workspaceFilePathCandidate";
import { normalizeAgentPatchText } from "./agentPatchMetadata";
import { isAgentUnifiedDiffText } from "./agentUnifiedDiffValidation";
export function buildAgentTurnSummaryPatchDiff(
batch: AgentTurnSummaryPatchBatchVM
): string {
return batch.changes
.map((change) => patchChangeToUnifiedDiff(change, batch.cwd))
.filter((diff) => diff.trim().length > 0)
.join("\n");
}
function patchChangeToUnifiedDiff(
change: AgentTurnSummaryPatchChangeVM,
cwd: string | null
): string {
if (patchChangeContainsBinaryContent(change)) {
return "";
}
const path = patchPathRelativeToCwd(change.path, cwd);
const rawDiff = normalizeAgentPatchText(change.unifiedDiff ?? "").trim();
if (rawDiff && isAgentUnifiedDiffText(rawDiff)) {
return ensureTrailingNewline(
wrapUnifiedDiff(path, change.changeType, rawDiff, cwd)
);
}
if (change.changeType === "created") {
return fileContentPatch(
path,
"created",
change.content ?? change.newString ?? rawDiff
);
}
if (change.changeType === "deleted") {
return fileContentPatch(
path,
"deleted",
change.oldString ?? change.content ?? rawDiff
);
}
if (change.oldString != null && change.newString != null) {
return modifiedFilePatch(path, change.oldString, change.newString);
}
return "";
}
function wrapUnifiedDiff(
path: string,
changeType: AgentTurnSummaryPatchChangeVM["changeType"],
diff: string,
cwd: string | null
): string {
if (diff.startsWith("diff --git ")) {
return rebaseUnifiedDiffPaths(diff, cwd);
}
const headers = [`diff --git a/${path} b/${path}`];
if (changeType === "created") {
headers.push("new file mode 100644", "--- /dev/null", `+++ b/${path}`);
} else if (changeType === "deleted") {
headers.push("deleted file mode 100644", `--- a/${path}`, "+++ /dev/null");
} else if (!diff.startsWith("--- ") && !diff.includes("\n--- ")) {
headers.push(`--- a/${path}`, `+++ b/${path}`);
}
return [...headers, diff].join("\n");
}
function rebaseUnifiedDiffPaths(diff: string, cwd: string | null): string {
let inHunk = false;
return diff
.split("\n")
.map((line) => {
if (line.startsWith("diff --git ")) {
inHunk = false;
} else if (line.startsWith("@@ ")) {
inHunk = true;
return line;
} else if (inHunk) {
return line;
}
const gitHeader = line.match(/^diff --git a\/(.+?) b\/(.+)$/);
if (gitHeader?.[1] && gitHeader[2]) {
return `diff --git a/${patchPathRelativeToCwd(
gitHeader[1],
cwd
)} b/${patchPathRelativeToCwd(gitHeader[2], cwd)}`;
}
const fileHeader = line.match(/^(---|\+\+\+) ((?:a\/|b\/)?)(.+)$/);
const [, marker, prefix, filePath] = fileHeader ?? [];
if (
!marker ||
prefix === undefined ||
!filePath ||
filePath === "/dev/null"
) {
return line;
}
return `${marker} ${prefix}${patchPathRelativeToCwd(filePath, cwd)}`;
})
.join("\n");
}
function fileContentPatch(
path: string,
changeType: "created" | "deleted",
content: string
): string {
if (content === "") {
return ensureTrailingNewline(
changeType === "created"
? [
`diff --git a/${path} b/${path}`,
"new file mode 100644",
"index 0000000..e69de29"
].join("\n")
: [
`diff --git a/${path} b/${path}`,
"deleted file mode 100644",
"index e69de29..0000000"
].join("\n")
);
}
const lines = splitPatchContentLines(content);
const count = Math.max(lines.length, 1);
const prefix = changeType === "created" ? "+" : "-";
const body =
lines.length > 0 ? lines.map((line) => `${prefix}${line}`) : [`${prefix}`];
const header =
changeType === "created"
? [
`diff --git a/${path} b/${path}`,
"new file mode 100644",
"--- /dev/null",
`+++ b/${path}`,
`@@ -0,0 +1,${count} @@`
]
: [
`diff --git a/${path} b/${path}`,
"deleted file mode 100644",
`--- a/${path}`,
"+++ /dev/null",
`@@ -1,${count} +0,0 @@`
];
return ensureTrailingNewline([...header, ...body].join("\n"));
}
function modifiedFilePatch(
path: string,
oldContent: string,
newContent: string
): string {
const oldLines = splitPatchContentLines(oldContent);
const newLines = splitPatchContentLines(newContent);
const oldCount = oldLines.length;
const newCount = newLines.length;
const oldStart = oldCount === 0 ? 0 : 1;
const newStart = newCount === 0 ? 0 : 1;
return ensureTrailingNewline(
[
`diff --git a/${path} b/${path}`,
`--- a/${path}`,
`+++ b/${path}`,
`@@ -${oldStart},${oldCount} +${newStart},${newCount} @@`,
...oldLines.map((line) => `-${line}`),
...newLines.map((line) => `+${line}`)
].join("\n")
);
}
function patchChangeContainsBinaryContent(
change: AgentTurnSummaryPatchChangeVM
): boolean {
return [
change.unifiedDiff,
change.content,
change.oldString,
change.newString
].some((value) => value?.includes("\0") === true);
}
function patchPathRelativeToCwd(path: string, cwd: string | null): string {
const normalizedCwd = normalizeWorkspaceFilePath(cwd ?? "");
const normalizedPath = normalizeWorkspaceFilePath(path, normalizedCwd);
if (
isAbsolutePatchPath(normalizedPath) &&
normalizedCwd &&
isInsideOrEqualWorkspaceFilePath(normalizedPath, normalizedCwd)
) {
const relativePath = normalizedPath
.slice(normalizedCwd.length)
.replace(/^\/+/, "");
if (relativePath) {
return relativePath;
}
}
return normalizedPath.replace(/^\/+/, "");
}
function isAbsolutePatchPath(path: string): boolean {
return path.startsWith("/") || /^\/?[A-Za-z]:\//.test(path);
}
function splitPatchContentLines(content: string): string[] {
if (!content) {
return [];
}
return content.replace(/\r\n/g, "\n").replace(/\n$/, "").split("\n");
}
function ensureTrailingNewline(value: string): string {
return value.endsWith("\n") ? value : `${value}\n`;
}