diff --git a/packages/coding-agent/src/core/tools/read.ts b/packages/coding-agent/src/core/tools/read.ts index 8a6c42f31..867d04dcf 100644 --- a/packages/coding-agent/src/core/tools/read.ts +++ b/packages/coding-agent/src/core/tools/read.ts @@ -10,6 +10,7 @@ import { keyHint, keyText } from "../../modes/interactive/components/keybinding- import { getLanguageFromPath, highlightCode, type Theme } from "../../modes/interactive/theme/theme.js"; import { formatDimensionNote, resizeImage } from "../../utils/image-resize.js"; import { detectSupportedImageMimeTypeFromFile } from "../../utils/mime.js"; +import { formatPathRelativeToCwdOrAbsolute } from "../../utils/paths.js"; import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.js"; import { resolveReadPath } from "./path-utils.js"; import { getTextOutput, invalidArgText, replaceTabs, shortenPath, str } from "./render-utils.js"; @@ -133,7 +134,7 @@ function getCompactReadClassification( if (docsClassification) return docsClassification; if (COMPACT_RESOURCE_FILE_NAMES.has(fileName)) { - return { kind: "resource", label: fileName }; + return { kind: "resource", label: formatPathRelativeToCwdOrAbsolute(absolutePath, cwd) }; } return undefined; diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 00183be84..e1cf20379 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -85,6 +85,7 @@ import { getChangelogPath, getNewEntries, parseChangelog } from "../../utils/cha import { copyToClipboard } from "../../utils/clipboard.js"; import { extensionForImageMimeType, readClipboardImage } from "../../utils/clipboard-image.js"; import { parseGitUrl } from "../../utils/git.js"; +import { getCwdRelativePath } from "../../utils/paths.js"; import { getPiUserAgent } from "../../utils/pi-user-agent.js"; import { killTrackedDetachedChildren } from "../../utils/shell.js"; import { ensureTool } from "../../utils/tools-manager.js"; @@ -911,15 +912,9 @@ export class InteractiveMode { private formatContextPath(p: string): string { const cwd = path.resolve(this.sessionManager.getCwd()); const absolutePath = path.isAbsolute(p) ? path.resolve(p) : path.resolve(cwd, p); - const relativePath = path.relative(cwd, absolutePath); - const isInsideCwd = - relativePath === "" || - (!relativePath.startsWith("..") && - !relativePath.startsWith(`..${path.sep}`) && - !path.isAbsolute(relativePath)); - - if (isInsideCwd) { - return relativePath || "."; + const relativePath = getCwdRelativePath(absolutePath, cwd); + if (relativePath !== undefined) { + return relativePath; } return this.formatDisplayPath(absolutePath); diff --git a/packages/coding-agent/src/utils/paths.ts b/packages/coding-agent/src/utils/paths.ts index 212e4f5ea..ef36de7a9 100644 --- a/packages/coding-agent/src/utils/paths.ts +++ b/packages/coding-agent/src/utils/paths.ts @@ -1,4 +1,5 @@ import { realpathSync } from "node:fs"; +import { isAbsolute, relative, resolve as resolvePath, sep } from "node:path"; /** * Resolve a path to its canonical (real) form, following symlinks. @@ -34,3 +35,23 @@ export function isLocalPath(value: string): boolean { } return true; } + +function resolveAgainstCwd(filePath: string, cwd: string): string { + return isAbsolute(filePath) ? resolvePath(filePath) : resolvePath(cwd, filePath); +} + +export function getCwdRelativePath(filePath: string, cwd: string): string | undefined { + const resolvedCwd = resolvePath(cwd); + const resolvedPath = resolveAgainstCwd(filePath, resolvedCwd); + const relativePath = relative(resolvedCwd, resolvedPath); + const isInsideCwd = + relativePath === "" || + (relativePath !== ".." && !relativePath.startsWith(`..${sep}`) && !isAbsolute(relativePath)); + + return isInsideCwd ? relativePath || "." : undefined; +} + +export function formatPathRelativeToCwdOrAbsolute(filePath: string, cwd: string): string { + const absolutePath = resolveAgainstCwd(filePath, cwd); + return (getCwdRelativePath(absolutePath, cwd) ?? absolutePath).split(sep).join("/"); +} diff --git a/packages/coding-agent/test/paths.test.ts b/packages/coding-agent/test/paths.test.ts index 5f7c115da..208da3f91 100644 --- a/packages/coding-agent/test/paths.test.ts +++ b/packages/coding-agent/test/paths.test.ts @@ -2,7 +2,7 @@ import { mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync, writeFileSyn import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, describe, expect, it } from "vitest"; -import { canonicalizePath, isLocalPath } from "../src/utils/paths.js"; +import { canonicalizePath, getCwdRelativePath, isLocalPath } from "../src/utils/paths.js"; let tempDir: string; @@ -61,6 +61,18 @@ describe("canonicalizePath", () => { }); }); +describe("getCwdRelativePath", () => { + it("keeps cwd-relative names that start with dots", () => { + const cwd = join(tmpdir(), "pi-paths-cwd"); + expect(getCwdRelativePath(join(cwd, "..config", "AGENTS.md"), cwd)).toBe(join("..config", "AGENTS.md")); + }); + + it("rejects parent-directory traversals", () => { + const cwd = join(tmpdir(), "pi-paths-cwd"); + expect(getCwdRelativePath(join(cwd, "..", "AGENTS.md"), cwd)).toBeUndefined(); + }); +}); + describe("isLocalPath", () => { it("returns true for bare names", () => { expect(isLocalPath("my-package")).toBe(true); diff --git a/packages/coding-agent/test/tool-execution-component.test.ts b/packages/coding-agent/test/tool-execution-component.test.ts index 2510b1adc..1e5d4b00e 100644 --- a/packages/coding-agent/test/tool-execution-component.test.ts +++ b/packages/coding-agent/test/tool-execution-component.test.ts @@ -1,4 +1,4 @@ -import { join } from "node:path"; +import { join, resolve } from "node:path"; import { Text, type TUI } from "@earendil-works/pi-tui"; import stripAnsi from "strip-ansi"; import { Type } from "typebox"; @@ -342,12 +342,20 @@ describe("ToolExecutionComponent parity", () => { }, { title: "AGENTS.md", - path: join(process.cwd(), "AGENTS.md"), + path: join(process.cwd(), ".pi", "AGENTS.md"), content: "Hidden resource instructions", - compact: "read resource AGENTS.md", + compact: "read resource .pi/AGENTS.md", hidden: "Hidden resource instructions", absent: undefined, }, + { + title: "outside AGENTS.md", + path: resolve(process.cwd(), "..", "AGENTS.md"), + content: "Hidden outside resource instructions", + compact: `read resource ${resolve(process.cwd(), "..", "AGENTS.md").replace(/\\/g, "/")}`, + hidden: "Hidden outside resource instructions", + absent: undefined, + }, { title: "Pi documentation", path: getReadmePath(),