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); }