Co-authored-by: Shubham Naik <shub@letta.com> Co-authored-by: Letta Code <noreply@letta.com>
199 lines
6.4 KiB
TypeScript
199 lines
6.4 KiB
TypeScript
/**
|
|
* Converts internal diff results (AdvancedDiffResult) to wire-safe DiffPreview
|
|
* for the bidirectional protocol. Strips full file contents (oldStr/newStr)
|
|
* and only sends hunks, which is sufficient for rendering.
|
|
*/
|
|
|
|
import path, { basename } from "node:path";
|
|
import type { AdvancedDiffResult, AdvancedHunk } from "../cli/helpers/diff";
|
|
import type { DiffHunk, DiffHunkLine, DiffPreview } from "../types/protocol_v2";
|
|
|
|
function parseHunkLinePrefix(raw: string): DiffHunkLine | null {
|
|
if (raw.length === 0) {
|
|
return { type: "context", content: "" };
|
|
}
|
|
if (raw[0] === "\\") {
|
|
// Metadata line (e.g. "\ No newline at end of file"), not a diff row.
|
|
return null;
|
|
}
|
|
const prefix = raw[0];
|
|
const content = raw.slice(1);
|
|
if (prefix === "+") return { type: "add", content };
|
|
if (prefix === "-") return { type: "remove", content };
|
|
if (prefix === " ") return { type: "context", content };
|
|
// Unknown prefix: preserve full line as context rather than dropping first char.
|
|
return { type: "context", content: raw };
|
|
}
|
|
|
|
function convertHunk(hunk: AdvancedHunk): DiffHunk {
|
|
const lines: DiffHunkLine[] = [];
|
|
for (const line of hunk.lines) {
|
|
const parsed = parseHunkLinePrefix(line.raw);
|
|
if (parsed) {
|
|
lines.push(parsed);
|
|
}
|
|
}
|
|
|
|
let oldLines = 0;
|
|
let newLines = 0;
|
|
for (const line of lines) {
|
|
if (line.type === "context") {
|
|
oldLines++;
|
|
newLines++;
|
|
} else if (line.type === "remove") {
|
|
oldLines++;
|
|
} else if (line.type === "add") {
|
|
newLines++;
|
|
}
|
|
}
|
|
|
|
return {
|
|
oldStart: hunk.oldStart,
|
|
oldLines,
|
|
newStart: hunk.newStart,
|
|
newLines,
|
|
lines,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Convert a single AdvancedDiffResult to a wire-safe DiffPreview.
|
|
* For multi-file patch tools, call this once per file operation.
|
|
*/
|
|
export function toDiffPreview(
|
|
result: AdvancedDiffResult,
|
|
fileNameOverride?: string,
|
|
): DiffPreview {
|
|
switch (result.mode) {
|
|
case "advanced":
|
|
return {
|
|
mode: "advanced",
|
|
fileName: fileNameOverride ?? result.fileName,
|
|
hunks: result.hunks.map(convertHunk),
|
|
};
|
|
case "fallback":
|
|
return {
|
|
mode: "fallback",
|
|
fileName: fileNameOverride ?? "unknown",
|
|
reason: result.reason,
|
|
};
|
|
case "unpreviewable":
|
|
return {
|
|
mode: "unpreviewable",
|
|
fileName: fileNameOverride ?? "unknown",
|
|
reason: result.reason,
|
|
};
|
|
}
|
|
}
|
|
|
|
type DiffDeps = {
|
|
computeAdvancedDiff: typeof import("../cli/helpers/diff").computeAdvancedDiff;
|
|
parsePatchToAdvancedDiff: typeof import("../cli/helpers/diff").parsePatchToAdvancedDiff;
|
|
isFileWriteTool: typeof import("../cli/helpers/toolNameMapping").isFileWriteTool;
|
|
isFileEditTool: typeof import("../cli/helpers/toolNameMapping").isFileEditTool;
|
|
isPatchTool: typeof import("../cli/helpers/toolNameMapping").isPatchTool;
|
|
parsePatchOperations: typeof import("../cli/helpers/formatArgsDisplay").parsePatchOperations;
|
|
};
|
|
|
|
let cachedDiffDeps: DiffDeps | null = null;
|
|
|
|
async function getDiffDeps(): Promise<DiffDeps> {
|
|
if (cachedDiffDeps) return cachedDiffDeps;
|
|
const [diffMod, toolNameMod, formatMod] = await Promise.all([
|
|
import("../cli/helpers/diff"),
|
|
import("../cli/helpers/toolNameMapping"),
|
|
import("../cli/helpers/formatArgsDisplay"),
|
|
]);
|
|
cachedDiffDeps = {
|
|
computeAdvancedDiff: diffMod.computeAdvancedDiff,
|
|
parsePatchToAdvancedDiff: diffMod.parsePatchToAdvancedDiff,
|
|
isFileWriteTool: toolNameMod.isFileWriteTool,
|
|
isFileEditTool: toolNameMod.isFileEditTool,
|
|
isPatchTool: toolNameMod.isPatchTool,
|
|
parsePatchOperations: formatMod.parsePatchOperations,
|
|
};
|
|
return cachedDiffDeps;
|
|
}
|
|
|
|
/**
|
|
* Compute diff previews for a tool call. Returns an array of DiffPreview
|
|
* (one per file for patch tools, one for Write/Edit tools).
|
|
*
|
|
* Mirrors the diff computation logic in App.tsx:4372-4438.
|
|
*/
|
|
export async function computeDiffPreviews(
|
|
toolName: string,
|
|
toolArgs: Record<string, unknown>,
|
|
workingDirectory: string = process.env.USER_CWD || process.cwd(),
|
|
): Promise<DiffPreview[]> {
|
|
const {
|
|
computeAdvancedDiff,
|
|
parsePatchToAdvancedDiff,
|
|
isFileWriteTool,
|
|
isFileEditTool,
|
|
isPatchTool,
|
|
parsePatchOperations,
|
|
} = await getDiffDeps();
|
|
const previews: DiffPreview[] = [];
|
|
|
|
try {
|
|
if (isFileWriteTool(toolName)) {
|
|
const filePath = toolArgs.file_path as string | undefined;
|
|
if (filePath) {
|
|
const resolvedFilePath = path.isAbsolute(filePath)
|
|
? filePath
|
|
: path.resolve(workingDirectory, filePath);
|
|
const result = computeAdvancedDiff({
|
|
kind: "write",
|
|
filePath: resolvedFilePath,
|
|
content: (toolArgs.content as string) || "",
|
|
});
|
|
previews.push(toDiffPreview(result, basename(filePath)));
|
|
}
|
|
} else if (isFileEditTool(toolName)) {
|
|
const filePath = toolArgs.file_path as string | undefined;
|
|
if (filePath) {
|
|
const resolvedFilePath = path.isAbsolute(filePath)
|
|
? filePath
|
|
: path.resolve(workingDirectory, filePath);
|
|
if (toolArgs.edits && Array.isArray(toolArgs.edits)) {
|
|
const result = computeAdvancedDiff({
|
|
kind: "multi_edit",
|
|
filePath: resolvedFilePath,
|
|
edits: toolArgs.edits as Array<{
|
|
old_string: string;
|
|
new_string: string;
|
|
replace_all?: boolean;
|
|
}>,
|
|
});
|
|
previews.push(toDiffPreview(result, basename(filePath)));
|
|
} else {
|
|
const result = computeAdvancedDiff({
|
|
kind: "edit",
|
|
filePath: resolvedFilePath,
|
|
oldString: (toolArgs.old_string as string) || "",
|
|
newString: (toolArgs.new_string as string) || "",
|
|
replaceAll: toolArgs.replace_all as boolean | undefined,
|
|
});
|
|
previews.push(toDiffPreview(result, basename(filePath)));
|
|
}
|
|
}
|
|
} else if (isPatchTool(toolName) && toolArgs.input) {
|
|
const operations = parsePatchOperations(toolArgs.input as string);
|
|
for (const op of operations) {
|
|
if (op.kind === "add" || op.kind === "update") {
|
|
const result = parsePatchToAdvancedDiff(op.patchLines, op.path);
|
|
if (result) {
|
|
previews.push(toDiffPreview(result, basename(op.path)));
|
|
}
|
|
}
|
|
// Delete operations don't produce diffs
|
|
}
|
|
}
|
|
} catch {
|
|
// Ignore diff computation errors — return whatever we have so far
|
|
}
|
|
|
|
return previews;
|
|
}
|