From d43300250de1a69984e01fd467f8846e27e40f48 Mon Sep 17 00:00:00 2001 From: Miroslav Drbal Date: Tue, 12 May 2026 14:55:58 +0200 Subject: [PATCH] fix(tools): resolve internal URLs in find tool before filesystem path lookup --- packages/coding-agent/src/tools/find.ts | 20 ++++++++++++++++++- .../test/tools/search-internal-urls.test.ts | 19 ++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/packages/coding-agent/src/tools/find.ts b/packages/coding-agent/src/tools/find.ts index 36da5a682..2f1027d9e 100644 --- a/packages/coding-agent/src/tools/find.ts +++ b/packages/coding-agent/src/tools/find.ts @@ -8,6 +8,7 @@ import { isEnoent, prompt, untilAborted } from "@oh-my-pi/pi-utils"; import type { Static } from "@sinclair/typebox"; import { Type } from "@sinclair/typebox"; import type { RenderResultOptions } from "../extensibility/custom-tools/types"; +import { InternalUrlRouter } from "../internal-urls"; import type { Theme } from "../modes/theme/theme"; import findDescription from "../prompts/tools/find.md" with { type: "text" }; import { type TruncationResult, truncateHead } from "../session/streaming-output"; @@ -25,6 +26,7 @@ import { applyListLimit } from "./list-limit"; import { formatFullOutputReference, type OutputMeta } from "./output-meta"; import { formatPathRelativeToCwd, + hasGlobPathChars, normalizePathLikeInput, parseFindPattern, partitionExistingPaths, @@ -116,7 +118,23 @@ export class FindTool implements AgentTool { return untilAborted(signal, async () => { const formatScopePath = (targetPath: string): string => formatPathRelativeToCwd(targetPath, this.session.cwd); - const normalizedPatterns = paths.map(input => normalizePathLikeInput(input).replace(/\\/g, "/")); + const rawPatterns = paths.map(input => normalizePathLikeInput(input).replace(/\\/g, "/")); + const internalRouter = InternalUrlRouter.instance(); + const normalizedPatterns: string[] = []; + for (const rawPattern of rawPatterns) { + if (!internalRouter.canHandle(rawPattern)) { + normalizedPatterns.push(rawPattern); + continue; + } + if (hasGlobPathChars(rawPattern)) { + throw new ToolError(`Glob patterns are not supported for internal URLs: ${rawPattern}`); + } + const resource = await internalRouter.resolve(rawPattern); + if (!resource.sourcePath) { + throw new ToolError(`Cannot find internal URL without a backing file: ${rawPattern}`); + } + normalizedPatterns.push(resource.sourcePath); + } if (normalizedPatterns.some(pattern => pattern.length === 0)) { throw new ToolError("`paths` must contain non-empty globs or paths"); } diff --git a/packages/coding-agent/test/tools/search-internal-urls.test.ts b/packages/coding-agent/test/tools/search-internal-urls.test.ts index da2928054..53006e3e3 100644 --- a/packages/coding-agent/test/tools/search-internal-urls.test.ts +++ b/packages/coding-agent/test/tools/search-internal-urls.test.ts @@ -5,6 +5,7 @@ import * as path from "node:path"; import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings"; import { InternalUrlRouter, LocalProtocolHandler } from "@oh-my-pi/pi-coding-agent/internal-urls"; import type { ToolSession } from "@oh-my-pi/pi-coding-agent/tools"; +import { FindTool } from "@oh-my-pi/pi-coding-agent/tools/find"; import { SearchTool } from "@oh-my-pi/pi-coding-agent/tools/search"; import { AgentRegistry } from "../../src/registry/agent-registry"; @@ -149,6 +150,24 @@ describe("SearchTool internal URL resolution", () => { expect(text).not.toMatch(/^\*?\s*\d+[a-z]{2}\|/m); }); + it("resolves local:// URLs before file-name lookup", async () => { + const localRoot = path.join(artifactsDir, "local"); + await fs.mkdir(localRoot, { recursive: true }); + await Bun.write(path.join(localRoot, "PLAN.md"), "# Plan\n"); + + LocalProtocolHandler.setOverride({ getArtifactsDir: () => artifactsDir, getSessionId: () => "session" }); + + const session = createSession(); + const tool = new FindTool(session); + + const result = await tool.execute("test-call", { + paths: ["local://PLAN.md"], + }); + + const text = getResultText(result); + expect(text).toContain("PLAN.md"); + }); + it("keeps hashline anchors when searching mutable local:// sources", async () => { const localRoot = path.join(artifactsDir, "local"); await fs.mkdir(localRoot, { recursive: true });