diff --git a/packages/app/src/assistant-file-links/parse.test.ts b/packages/app/src/assistant-file-links/parse.test.ts index 9410f3da4..d3dc823cb 100644 --- a/packages/app/src/assistant-file-links/parse.test.ts +++ b/packages/app/src/assistant-file-links/parse.test.ts @@ -360,6 +360,25 @@ describe("parseAssistantFileLink", () => { }), ).toBeNull(); }); + + it("does not throw when the input contains a literal '%' that is not a valid percent-escape", () => { + // Regressions for tool output strings like ping's "100% packet loss", + // Windows "%PATH%" references, and percentages such as "0% off". + // decodeURIComponent throws URIError on these; the parser must swallow + // it and return null rather than crash the renderer. + const cases = [ + "/tmp/100% packet loss", + "/Users/test/project/0% off", + "/var/log/%PATH%/x.log", + "file:///tmp/100% packet loss", + ]; + for (const value of cases) { + expect(() => + parseAssistantFileLink(value, { workspaceRoot: "/Users/test/project" }), + ).not.toThrow(); + } + expect(() => parseFileProtocolUrl("file:///tmp/100% packet loss")).not.toThrow(); + }); }); describe("normalizeInlinePathTarget", () => { diff --git a/packages/app/src/assistant-file-links/parse.ts b/packages/app/src/assistant-file-links/parse.ts index c46a67de1..bfcf31842 100644 --- a/packages/app/src/assistant-file-links/parse.ts +++ b/packages/app/src/assistant-file-links/parse.ts @@ -1,5 +1,13 @@ import { isAbsolutePath } from "@/utils/path"; +function safeDecodeURIComponent(value: string): string { + try { + return decodeURIComponent(value); + } catch { + return value; + } +} + export interface InlinePathTarget { raw: string; path: string; @@ -350,7 +358,7 @@ export function parseAssistantFileLink( return null; } - const normalizedPath = normalizePathToken(decodeURIComponent(parsedUrl.pathname)); + const normalizedPath = normalizePathToken(safeDecodeURIComponent(parsedUrl.pathname)); if (!normalizedPath || !isAbsolutePath(normalizedPath)) { return null; } @@ -659,7 +667,7 @@ function normalizeFileUrlPath(pathname: string): string | null { return null; } - const decoded = decodeURIComponent(pathname).replace(/\\/g, "/"); + const decoded = safeDecodeURIComponent(pathname).replace(/\\/g, "/"); if (!decoded) { return null; }