fix: preview edits through configured backend

This commit is contained in:
Peter Steinberger
2026-05-27 06:42:03 +01:00
parent ea46d3607d
commit 14e863e234
3 changed files with 62 additions and 5 deletions

View File

@@ -423,6 +423,11 @@ export interface EditDiffError {
error: string;
}
export interface EditDiffOperations {
readFile: (absolutePath: string) => Promise<Buffer | string>;
access: (absolutePath: string) => Promise<void>;
}
/**
* Compute the diff for one or more edit operations without applying them.
* Used for preview rendering in the TUI before the tool executes.
@@ -431,13 +436,18 @@ export async function computeEditsDiff(
path: string,
edits: Edit[],
cwd: string,
operations?: EditDiffOperations,
): Promise<EditDiffResult | EditDiffError> {
const absolutePath = resolveToCwd(path, cwd);
try {
// Check if file exists and is readable
try {
await access(absolutePath, constants.R_OK);
if (operations) {
await operations.access(absolutePath);
} else {
await access(absolutePath, constants.R_OK);
}
} catch (error: unknown) {
const errorMessage =
error instanceof Error && "code" in error
@@ -447,7 +457,11 @@ export async function computeEditsDiff(
}
// Read the file
const rawContent = await readFile(absolutePath, "utf-8");
const rawContentResult = operations
? await operations.readFile(absolutePath)
: await readFile(absolutePath, "utf-8");
const rawContent =
typeof rawContentResult === "string" ? rawContentResult : rawContentResult.toString("utf-8");
// Strip BOM before matching (LLM won't include invisible BOM in oldText)
const { text: content } = stripBom(rawContent);

View File

@@ -1,8 +1,15 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { createEditTool, type EditOperations } from "./edit.js";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { Theme } from "../../modes/interactive/theme/theme.js";
import { createEditTool, createEditToolDefinition, type EditOperations } from "./edit.js";
const testTheme = {
bg: (_name: string, text: string) => text,
bold: (text: string) => text,
fg: (_name: string, text: string) => text,
} as Theme;
describe("edit tool", () => {
let tmpDir = "";
@@ -125,4 +132,40 @@ describe("edit tool", () => {
text: `Successfully replaced 2 block(s) in ${filePath}.`,
});
});
it("renders previews through custom edit operations", async () => {
const readFile = vi.fn(async () => Buffer.from("remote original\n"));
const operations: EditOperations = {
access: async () => {},
readFile,
writeFile: async () => {},
};
const tool = createEditToolDefinition("/workspace", { operations });
const args = {
path: "remote.txt",
edits: [{ oldText: "remote original", newText: "remote changed" }],
};
const context = {
args,
argsComplete: true,
cwd: "/workspace",
executionStarted: false,
expanded: false,
invalidate: vi.fn(),
isError: false,
isPartial: false,
lastComponent: undefined,
showImages: false,
state: {},
toolCallId: "call-preview",
};
const component = tool.renderCall?.(args, testTheme, context);
await vi.waitFor(() => expect(context.invalidate).toHaveBeenCalled());
expect(readFile).toHaveBeenCalledWith(path.join("/workspace", "remote.txt"));
expect((component as { preview?: { diff?: string } } | undefined)?.preview?.diff).toContain(
"remote changed",
);
});
});

View File

@@ -471,7 +471,7 @@ export function createEditToolDefinition(
if (context.argsComplete && previewInput && !component.preview && !component.previewPending) {
component.previewPending = true;
const requestKey = argsKey;
void computeEditsDiff(previewInput.path, previewInput.edits, context.cwd).then(
void computeEditsDiff(previewInput.path, previewInput.edits, context.cwd, ops).then(
(preview) => {
if (component.previewArgsKey === requestKey) {
setEditPreview(component, preview, requestKey);