From 14b24032e976153538680ffa4a63b7fa56453605 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E5=BF=97?= Date: Thu, 12 Mar 2026 20:04:34 +0800 Subject: [PATCH] fix: avoid file-link parsing for natural language slash phrases --- .../messages/components/Markdown.test.tsx | 39 +++++++++++++++++++ src/utils/remarkFileLinks.ts | 23 ++++------- 2 files changed, 46 insertions(+), 16 deletions(-) diff --git a/src/features/messages/components/Markdown.test.tsx b/src/features/messages/components/Markdown.test.tsx index 5a76f69a7..dd75ce237 100644 --- a/src/features/messages/components/Markdown.test.tsx +++ b/src/features/messages/components/Markdown.test.tsx @@ -273,4 +273,43 @@ describe("Markdown file-like href behavior", () => { expect(clickEvent.defaultPrevented).toBe(true); expect(onOpenFileLink).not.toHaveBeenCalled(); }); + + it("does not turn natural-language slash phrases into file links", () => { + const { container } = render( + , + ); + + expect(container.querySelector(".message-file-link")).toBeNull(); + expect(container.textContent).toContain("app/daemon"); + expect(container.textContent).toContain("Git/Plan"); + }); + + it("does not turn longer slash phrases into file links", () => { + const { container } = render( + , + ); + + expect(container.querySelector(".message-file-link")).toBeNull(); + expect(container.textContent).toContain("Spec/Verification/Evidence"); + }); + + it("still turns clear file paths in plain text into file links", () => { + const { container } = render( + , + ); + + const fileLinks = [...container.querySelectorAll(".message-file-link")]; + expect(fileLinks).toHaveLength(2); + expect(fileLinks[0]?.textContent).toContain("setup.md"); + expect(fileLinks[1]?.textContent).toContain("index.ts"); + }); }); diff --git a/src/utils/remarkFileLinks.ts b/src/utils/remarkFileLinks.ts index ca9682df9..4824452fa 100644 --- a/src/utils/remarkFileLinks.ts +++ b/src/utils/remarkFileLinks.ts @@ -9,17 +9,7 @@ const FILE_PATH_PATTERN = const FILE_PATH_MATCH = new RegExp(`^${FILE_PATH_PATTERN.source}$`); const TRAILING_PUNCTUATION = new Set([".", ",", ";", ":", "!", "?", ")", "]", "}"]); -const RELATIVE_ALLOWED_PREFIXES = [ - "src/", - "app/", - "lib/", - "tests/", - "test/", - "packages/", - "apps/", - "docs/", - "scripts/", -]; +const LETTER_OR_NUMBER_PATTERN = /[\p{L}\p{N}.]/u; type MarkdownNode = { type: string; @@ -43,7 +33,11 @@ function isPathCandidate( return false; } if (value.startsWith("/") || value.startsWith("./") || value.startsWith("../")) { - if (value.startsWith("/") && previousChar && /[A-Za-z0-9.]/.test(previousChar)) { + if ( + value.startsWith("/") && + previousChar && + LETTER_OR_NUMBER_PATTERN.test(previousChar) + ) { return false; } return true; @@ -52,10 +46,7 @@ function isPathCandidate( return true; } const lastSegment = value.split("/").pop() ?? ""; - if (lastSegment.includes(".")) { - return true; - } - return RELATIVE_ALLOWED_PREFIXES.some((prefix) => value.startsWith(prefix)); + return lastSegment.includes("."); } function splitTrailingPunctuation(value: string) {