Skip to content
Merged
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 @@ -35,6 +35,20 @@ describe("createReadFileRangeTool", () => {
// Happy paths
// --------------------------------------------------------------------------

it("prefers unsaved editor buffer text over on-disk content", async () => {
const cwd = makeTmpDir("read-dirty-");
writeFixtureFile(cwd, "dirty.ts", "saved on disk");

const tool = createReadFileRangeTool(cwd, {
getDirtyFileTextForPath: () => "unsaved in editor",
});
const result = await tool.execute({ file_path: "dirty.ts" });

expect(result.error).toBeUndefined();
expect(result.content).toContain("unsaved in editor");
expect(result.content).not.toContain("saved on disk");
});

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
21 changes: 18 additions & 3 deletions apps/desktop/src/main/services/ai/tools/readFileRange.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,22 @@ import { executableTool as tool } from "./executableTool";
import { z } from "zod";
import fs from "node:fs";
import path from "node:path";
import { getErrorMessage, readFileWithinRootSecure, resolvePathWithinRoot } from "../../shared/utils";
import {
getErrorMessage,
readAgentAccessibleFileBytes,
resolvePathWithinRoot,
type DirtyFileTextLookup,
} from "../../shared/utils";

function toDisplayPath(root: string, filePath: string): string {
return path.relative(root, filePath).replace(/\\/g, "/");
}

export function createReadFileRangeTool(cwd: string) {
export type ReadFileRangeToolOptions = {
getDirtyFileTextForPath?: DirtyFileTextLookup;
};

export function createReadFileRangeTool(cwd: string, options: ReadFileRangeToolOptions = {}) {
return tool({
description:
"Read a file's contents with line numbers. Accepts an absolute path or a path relative to the active repo root.",
Expand Down Expand Up @@ -40,7 +49,13 @@ export function createReadFileRangeTool(cwd: string) {
return { content: "", totalLines: 0, error: `Error reading file: ${message}` };
}

const raw = readFileWithinRootSecure(root, file_path).toString("utf-8");
const raw = (
await readAgentAccessibleFileBytes({
rootPath: root,
resolvedPath: resolvedPath,
getDirtyFileTextForPath: options.getDirtyFileTextForPath,
})
).toString("utf-8");
const allLines = raw.split("\n");
const totalLines = allLines.length;

Expand Down
14 changes: 12 additions & 2 deletions apps/desktop/src/main/services/ai/tools/universalTools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,13 @@ import { webFetchTool } from "./webFetch";
import { webSearchTool } from "./webSearch";
import type { AgentChatApprovalDecision, AgentChatEvent, PendingInputKind, WorkerSandboxConfig } from "../../../../shared/types";
import { DEFAULT_WORKER_SANDBOX_CONFIG } from "./workerSandboxDefaults";
import { getErrorMessage, isEnoentError, isWithinDir, resolvePathWithinRoot } from "../../shared/utils";
import {
getErrorMessage,
isEnoentError,
isWithinDir,
resolvePathWithinRoot,
type DirtyFileTextLookup,
} from "../../shared/utils";
import { terminateProcessTree } from "../../shared/processExecution";

const execFileAsync = promisify(execFile);
Expand Down Expand Up @@ -75,6 +81,8 @@ export interface UniversalToolSetOptions {
* controller and abort it when an external policy event cancels the session.
*/
registerActiveBash?: (controller: AbortController) => (() => void) | void;
/** Prefer unsaved Files-tab editor buffers over on-disk content for readFile. */
getDirtyFileTextForPath?: DirtyFileTextLookup;
}

// ── Permission helpers ──────────────────────────────────────────────
Expand Down Expand Up @@ -2708,7 +2716,9 @@ export function createUniversalToolSet(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const tools: Record<string, Tool<any, any>> = {
// Read-only tools (auto-allowed in all modes)
readFile: createReadFileRangeTool(cwd),
readFile: createReadFileRangeTool(cwd, {
getDirtyFileTextForPath: opts.getDirtyFileTextForPath,
}),
grep: createGrepSearchTool(cwd),
glob: createGlobSearchTool(cwd),
listDir: createListDirTool(cwd),
Expand Down
Loading
Loading