diff --git a/src/agents/agent-tools.read.ts b/src/agents/agent-tools.read.ts index 97a807d2fe50..c50cad808636 100644 --- a/src/agents/agent-tools.read.ts +++ b/src/agents/agent-tools.read.ts @@ -50,7 +50,7 @@ import { type ReadToolDetails, type ReadToolTruncationDetails, } from "./sessions/tools/index.js"; -import { expandOsHomePrefix, resolveReadPath } from "./sessions/tools/path-utils.js"; +import { expandOsHomePrefix, resolveToCwd } from "./sessions/tools/path-utils.js"; import { createBoundedReadTextPage, formatReadContinuationNotice } from "./sessions/tools/read.js"; import { ReadToolContinuationSchema, @@ -1080,7 +1080,7 @@ export function wrapReadToolWithSkillContent( root: cwd, containerWorkdir: options?.containerWorkdir, }); - return resolveReadPath(mapped, cwd); + return resolveToCwd(mapped, cwd); }; const instructionContent = new Map( (options?.instructionPaths ?? []).map((filePath) => [ diff --git a/src/agents/sessions/tools/path-utils.test.ts b/src/agents/sessions/tools/path-utils.test.ts index c20574b4be2b..602a04ab773b 100644 --- a/src/agents/sessions/tools/path-utils.test.ts +++ b/src/agents/sessions/tools/path-utils.test.ts @@ -2,37 +2,44 @@ import os from "node:os"; import path from "node:path"; import { pathToFileURL } from "node:url"; import { describe, expect, it } from "vitest"; -import { resolveReadPath } from "./path-utils.js"; +import { resolveToCwd } from "./path-utils.js"; -describe("resolveReadPath", () => { +describe("resolveToCwd", () => { const cwd = path.resolve("workspace"); it("resolves ordinary relative paths against cwd", () => { - expect(resolveReadPath("notes/today.md", cwd)).toBe(path.resolve(cwd, "notes/today.md")); + expect(resolveToCwd("notes/today.md", cwd)).toBe(path.resolve(cwd, "notes/today.md")); + }); + + it("keeps Unicode spaces in the destination path", () => { + const nnbsp = "Screenshot 9.30\u202FAM.png"; + const ascii = "Screenshot 9.30 AM.png"; + expect(resolveToCwd(nnbsp, cwd)).toBe(path.resolve(cwd, nnbsp)); + expect(resolveToCwd(nnbsp, cwd)).not.toBe(path.resolve(cwd, ascii)); }); it("resolves valid file URLs to their filesystem path", () => { const target = path.resolve(cwd, "notes.txt"); - expect(resolveReadPath(pathToFileURL(target).href, cwd)).toBe(target); + expect(resolveToCwd(pathToFileURL(target).href, cwd)).toBe(target); }); it("keeps malformed file URLs on the ordinary relative-path path", () => { const malformed = "file://%"; - expect(resolveReadPath(malformed, cwd)).toBe(path.resolve(cwd, malformed)); + expect(resolveToCwd(malformed, cwd)).toBe(path.resolve(cwd, malformed)); }); it.runIf(process.platform === "win32")( "expands a Windows-style home prefix against the OS home", () => { const homeDir = process.env.HOME ?? os.homedir(); - expect(resolveReadPath("~\\notes.txt", cwd)).toBe(path.resolve(homeDir, "notes.txt")); + expect(resolveToCwd("~\\notes.txt", cwd)).toBe(path.resolve(homeDir, "notes.txt")); }, ); it.runIf(process.platform !== "win32")( "keeps a backslash-prefixed tilde literal on POSIX", () => { - expect(resolveReadPath("~\\notes.txt", cwd)).toBe(path.resolve(cwd, "~\\notes.txt")); + expect(resolveToCwd("~\\notes.txt", cwd)).toBe(path.resolve(cwd, "~\\notes.txt")); }, ); }); diff --git a/src/agents/sessions/tools/path-utils.ts b/src/agents/sessions/tools/path-utils.ts index 8259397fef58..78e543f9843c 100644 --- a/src/agents/sessions/tools/path-utils.ts +++ b/src/agents/sessions/tools/path-utils.ts @@ -34,9 +34,8 @@ export function expandOsHomePrefix(filePath: string): string { return home ? expandHomePrefix(filePath, { home }) : filePath; } -function expandPath(filePath: string, normalizeSpaces = true): string { - const withoutAtPrefix = normalizeAtPrefix(filePath); - const normalized = normalizeSpaces ? normalizeUnicodeSpaces(withoutAtPrefix) : withoutAtPrefix; +function expandPath(filePath: string): string { + const normalized = normalizeAtPrefix(filePath); if (normalized.startsWith("file://")) { try { return fileURLToPath(normalized); @@ -53,14 +52,6 @@ function expandPath(filePath: string, normalizeSpaces = true): string { */ export function resolveToCwd(filePath: string, cwd: string): string { const expanded = expandPath(filePath); - if (isAbsolute(expanded)) { - return expanded; - } - return resolvePath(cwd, expanded); -} - -export function resolveReadPath(filePath: string, cwd: string): string { - const expanded = expandPath(filePath, false); return isAbsolute(expanded) ? expanded : resolvePath(cwd, expanded); } diff --git a/src/agents/sessions/tools/read.ts b/src/agents/sessions/tools/read.ts index 47af2f91c4c1..26d76badbe9f 100644 --- a/src/agents/sessions/tools/read.ts +++ b/src/agents/sessions/tools/read.ts @@ -32,7 +32,7 @@ import { detectSupportedImageMimeType } from "../../utils/mime.js"; import { formatPathRelativeToCwdOrAbsolute } from "../../utils/paths.js"; import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.js"; import { normalizePositiveLimit } from "./limits.js"; -import { getReadPathVariants, resolveReadPath } from "./path-utils.js"; +import { getReadPathVariants, resolveToCwd } from "./path-utils.js"; import { createReadToolDetails, readToolInputSchema, @@ -216,7 +216,7 @@ function getCompactReadClassification( return undefined; } - const absolutePath = resolveReadPath(rawPath, cwd); + const absolutePath = resolveToCwd(rawPath, cwd); const fileName = basename(absolutePath); if (fileName === "SKILL.md") { return { kind: "skill", label: basename(dirname(absolutePath)) || fileName }; @@ -239,7 +239,7 @@ async function resolveLocalReadPath(filePath: string, cwd: string): Promise { - const absolutePath = await (ops.resolvePath?.(filePath, cwd) ?? resolveReadPath(filePath, cwd)); + const absolutePath = await (ops.resolvePath?.(filePath, cwd) ?? resolveToCwd(filePath, cwd)); try { await ops.access(absolutePath); return { absolutePath }; diff --git a/src/agents/sessions/tools/write.test.ts b/src/agents/sessions/tools/write.test.ts index 18ff56d6303d..761c14a43851 100644 --- a/src/agents/sessions/tools/write.test.ts +++ b/src/agents/sessions/tools/write.test.ts @@ -178,6 +178,18 @@ describe("write tool", () => { await expect(fs.readFile(filePath, "utf-8")).resolves.toBe("finished\n"); }); + it("writes the literal Unicode-space path instead of an ASCII-space sibling", async () => { + const nnbspPath = await createTempPath("report 2026.md"); + const asciiPath = path.join(tmpDir, "report 2026.md"); + await fs.writeFile(asciiPath, "ascii\n", "utf-8"); + const tool = createWriteTool(tmpDir); + + await tool.execute("call-1", { path: nnbspPath, content: "nnbsp\n" }, undefined); + + await expect(fs.readFile(nnbspPath, "utf-8")).resolves.toBe("nnbsp\n"); + await expect(fs.readFile(asciiPath, "utf-8")).resolves.toBe("ascii\n"); + }); + it("returns terminal no-op when writing identical content to existing file", async () => { const filePath = await createTempPath("identical.txt"); await fs.writeFile(filePath, "hello\n", "utf-8");