fix(coding-agent): disambiguate resource paths
This commit is contained in:
@@ -10,6 +10,7 @@ import { keyHint, keyText } from "../../modes/interactive/components/keybinding-
|
|||||||
import { getLanguageFromPath, highlightCode, type Theme } from "../../modes/interactive/theme/theme.js";
|
import { getLanguageFromPath, highlightCode, type Theme } from "../../modes/interactive/theme/theme.js";
|
||||||
import { formatDimensionNote, resizeImage } from "../../utils/image-resize.js";
|
import { formatDimensionNote, resizeImage } from "../../utils/image-resize.js";
|
||||||
import { detectSupportedImageMimeTypeFromFile } from "../../utils/mime.js";
|
import { detectSupportedImageMimeTypeFromFile } from "../../utils/mime.js";
|
||||||
|
import { formatPathRelativeToCwdOrAbsolute } from "../../utils/paths.js";
|
||||||
import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.js";
|
import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.js";
|
||||||
import { resolveReadPath } from "./path-utils.js";
|
import { resolveReadPath } from "./path-utils.js";
|
||||||
import { getTextOutput, invalidArgText, replaceTabs, shortenPath, str } from "./render-utils.js";
|
import { getTextOutput, invalidArgText, replaceTabs, shortenPath, str } from "./render-utils.js";
|
||||||
@@ -133,7 +134,7 @@ function getCompactReadClassification(
|
|||||||
if (docsClassification) return docsClassification;
|
if (docsClassification) return docsClassification;
|
||||||
|
|
||||||
if (COMPACT_RESOURCE_FILE_NAMES.has(fileName)) {
|
if (COMPACT_RESOURCE_FILE_NAMES.has(fileName)) {
|
||||||
return { kind: "resource", label: fileName };
|
return { kind: "resource", label: formatPathRelativeToCwdOrAbsolute(absolutePath, cwd) };
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ import { getChangelogPath, getNewEntries, parseChangelog } from "../../utils/cha
|
|||||||
import { copyToClipboard } from "../../utils/clipboard.js";
|
import { copyToClipboard } from "../../utils/clipboard.js";
|
||||||
import { extensionForImageMimeType, readClipboardImage } from "../../utils/clipboard-image.js";
|
import { extensionForImageMimeType, readClipboardImage } from "../../utils/clipboard-image.js";
|
||||||
import { parseGitUrl } from "../../utils/git.js";
|
import { parseGitUrl } from "../../utils/git.js";
|
||||||
|
import { getCwdRelativePath } from "../../utils/paths.js";
|
||||||
import { getPiUserAgent } from "../../utils/pi-user-agent.js";
|
import { getPiUserAgent } from "../../utils/pi-user-agent.js";
|
||||||
import { killTrackedDetachedChildren } from "../../utils/shell.js";
|
import { killTrackedDetachedChildren } from "../../utils/shell.js";
|
||||||
import { ensureTool } from "../../utils/tools-manager.js";
|
import { ensureTool } from "../../utils/tools-manager.js";
|
||||||
@@ -911,15 +912,9 @@ export class InteractiveMode {
|
|||||||
private formatContextPath(p: string): string {
|
private formatContextPath(p: string): string {
|
||||||
const cwd = path.resolve(this.sessionManager.getCwd());
|
const cwd = path.resolve(this.sessionManager.getCwd());
|
||||||
const absolutePath = path.isAbsolute(p) ? path.resolve(p) : path.resolve(cwd, p);
|
const absolutePath = path.isAbsolute(p) ? path.resolve(p) : path.resolve(cwd, p);
|
||||||
const relativePath = path.relative(cwd, absolutePath);
|
const relativePath = getCwdRelativePath(absolutePath, cwd);
|
||||||
const isInsideCwd =
|
if (relativePath !== undefined) {
|
||||||
relativePath === "" ||
|
return relativePath;
|
||||||
(!relativePath.startsWith("..") &&
|
|
||||||
!relativePath.startsWith(`..${path.sep}`) &&
|
|
||||||
!path.isAbsolute(relativePath));
|
|
||||||
|
|
||||||
if (isInsideCwd) {
|
|
||||||
return relativePath || ".";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.formatDisplayPath(absolutePath);
|
return this.formatDisplayPath(absolutePath);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { realpathSync } from "node:fs";
|
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.
|
* Resolve a path to its canonical (real) form, following symlinks.
|
||||||
@@ -34,3 +35,23 @@ export function isLocalPath(value: string): boolean {
|
|||||||
}
|
}
|
||||||
return true;
|
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("/");
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync, writeFileSyn
|
|||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { afterEach, describe, expect, it } from "vitest";
|
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;
|
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", () => {
|
describe("isLocalPath", () => {
|
||||||
it("returns true for bare names", () => {
|
it("returns true for bare names", () => {
|
||||||
expect(isLocalPath("my-package")).toBe(true);
|
expect(isLocalPath("my-package")).toBe(true);
|
||||||
|
|||||||
@@ -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 { Text, type TUI } from "@earendil-works/pi-tui";
|
||||||
import stripAnsi from "strip-ansi";
|
import stripAnsi from "strip-ansi";
|
||||||
import { Type } from "typebox";
|
import { Type } from "typebox";
|
||||||
@@ -342,12 +342,20 @@ describe("ToolExecutionComponent parity", () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "AGENTS.md",
|
title: "AGENTS.md",
|
||||||
path: join(process.cwd(), "AGENTS.md"),
|
path: join(process.cwd(), ".pi", "AGENTS.md"),
|
||||||
content: "Hidden resource instructions",
|
content: "Hidden resource instructions",
|
||||||
compact: "read resource AGENTS.md",
|
compact: "read resource .pi/AGENTS.md",
|
||||||
hidden: "Hidden resource instructions",
|
hidden: "Hidden resource instructions",
|
||||||
absent: undefined,
|
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",
|
title: "Pi documentation",
|
||||||
path: getReadmePath(),
|
path: getReadmePath(),
|
||||||
|
|||||||
Reference in New Issue
Block a user