From 611b53945954e3f4f5965c9ce888e494ae48a48e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 29 May 2026 08:04:51 +0000 Subject: [PATCH] Fix agent reads for unsaved new files not yet on disk readAgentAccessibleFileBytes and readFileRange required paths to exist before consulting dirty editor buffers. New unsaved files in the Files tab never reached the dirty lookup, so agents still saw file-not-found after #393. Co-authored-by: Arul Sharma --- .../services/ai/tools/readFileRange.test.ts | 14 ++++++++++ .../main/services/ai/tools/readFileRange.ts | 27 +++++++++++++------ .../src/main/services/shared/utils.test.ts | 17 ++++++++++++ .../desktop/src/main/services/shared/utils.ts | 3 ++- 4 files changed, 52 insertions(+), 9 deletions(-) diff --git a/apps/desktop/src/main/services/ai/tools/readFileRange.test.ts b/apps/desktop/src/main/services/ai/tools/readFileRange.test.ts index 569dcf978..5245a8c2b 100644 --- a/apps/desktop/src/main/services/ai/tools/readFileRange.test.ts +++ b/apps/desktop/src/main/services/ai/tools/readFileRange.test.ts @@ -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); diff --git a/apps/desktop/src/main/services/ai/tools/readFileRange.ts b/apps/desktop/src/main/services/ai/tools/readFileRange.ts index d2f619dd8..230bc5c16 100644 --- a/apps/desktop/src/main/services/ai/tools/readFileRange.ts +++ b/apps/desktop/src/main/services/ai/tools/readFileRange.ts @@ -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:")) { @@ -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; diff --git a/apps/desktop/src/main/services/shared/utils.test.ts b/apps/desktop/src/main/services/shared/utils.test.ts index 56926f95b..d1534239b 100644 --- a/apps/desktop/src/main/services/shared/utils.test.ts +++ b/apps/desktop/src/main/services/shared/utils.test.ts @@ -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-")); diff --git a/apps/desktop/src/main/services/shared/utils.ts b/apps/desktop/src/main/services/shared/utils.ts index cba941536..c38de6547 100644 --- a/apps/desktop/src/main/services/shared/utils.ts +++ b/apps/desktop/src/main/services/shared/utils.ts @@ -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); }