Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions apps/desktop/src/main/services/ai/tools/readFileRange.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,20 @@ describe("createReadFileRangeTool", () => {
expect(result.content).not.toContain("saved on disk");
});

it("reads unsaved new files that are not on disk yet", async () => {
const cwd = makeTmpDir("read-dirty-new-");
const absPath = path.join(cwd, "src", "new.ts");

const tool = createReadFileRangeTool(cwd, {
getDirtyFileTextForPath: (candidate) =>
(candidate === absPath ? "draft only in editor" : undefined),
});
const result = await tool.execute({ file_path: "src/new.ts" });

expect(result.error).toBeUndefined();
expect(result.content).toContain("draft only in editor");
});

it("reads an entire file when no offset or limit is given", async () => {
const cwd = makeTmpDir("read-full-");
writeFixtureFile(cwd, "sample.ts", FIVE_LINES);
Expand Down
27 changes: 19 additions & 8 deletions apps/desktop/src/main/services/ai/tools/readFileRange.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ export function createReadFileRangeTool(cwd: string, options: ReadFileRangeToolO
const root = fs.realpathSync(cwd);
let resolvedPath: string;
try {
resolvedPath = resolvePathWithinRoot(root, file_path, { allowMissing: false });
resolvedPath = resolvePathWithinRoot(root, file_path, {
allowMissing: Boolean(options.getDirtyFileTextForPath),
});
} catch (error) {
const message = getErrorMessage(error);
if (message.startsWith("Path does not exist:")) {
Expand All @@ -49,13 +51,22 @@ export function createReadFileRangeTool(cwd: string, options: ReadFileRangeToolO
return { content: "", totalLines: 0, error: `Error reading file: ${message}` };
}

const raw = (
await readAgentAccessibleFileBytes({
rootPath: root,
resolvedPath: resolvedPath,
getDirtyFileTextForPath: options.getDirtyFileTextForPath,
})
).toString("utf-8");
let raw: string;
try {
raw = (
await readAgentAccessibleFileBytes({
rootPath: root,
resolvedPath: resolvedPath,
getDirtyFileTextForPath: options.getDirtyFileTextForPath,
})
).toString("utf-8");
} catch (error) {
const message = getErrorMessage(error);
if (message.startsWith("Path does not exist:") || (error as NodeJS.ErrnoException).code === "ENOENT") {
return { content: "", totalLines: 0, error: `File not found: ${file_path}` };
}
throw error;
}
const allLines = raw.split("\n");
const totalLines = allLines.length;

Expand Down
17 changes: 17 additions & 0 deletions apps/desktop/src/main/services/shared/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,23 @@ describe("readAgentAccessibleFileBytes", () => {
}
});

it("returns dirty editor text for paths that do not exist on disk yet", async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "ade-dirty-read-new-"));
try {
const filePath = path.join(root, "src", "new.ts");

const bytes = await readAgentAccessibleFileBytes({
rootPath: root,
resolvedPath: filePath,
getDirtyFileTextForPath: () => "unsaved new file",
});

expect(bytes.toString("utf8")).toBe("unsaved new file");
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
});

it("does not let dirty-buffer lookup bypass the workspace boundary", async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "ade-dirty-read-root-"));
const outside = fs.mkdtempSync(path.join(os.tmpdir(), "ade-dirty-read-outside-"));
Expand Down
3 changes: 2 additions & 1 deletion apps/desktop/src/main/services/shared/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -476,7 +476,8 @@ export async function readAgentAccessibleFileBytes(args: {
const root = path.resolve(args.rootPath);
let absPath: string;
try {
absPath = resolvePathWithinRoot(root, args.resolvedPath, { allowMissing: false });
// allowMissing so unsaved new files (not yet on disk) can still resolve for dirty lookup.
absPath = resolvePathWithinRoot(root, args.resolvedPath, { allowMissing: true });
} catch {
return readFileWithinRootSecure(root, args.resolvedPath);
}
Expand Down
Loading