diff --git a/webui/src/components/FileReferenceChip.tsx b/webui/src/components/FileReferenceChip.tsx index dac9baa7f..5526fec7d 100644 --- a/webui/src/components/FileReferenceChip.tsx +++ b/webui/src/components/FileReferenceChip.tsx @@ -127,6 +127,7 @@ export function isLikelyFilePath(value: string): boolean { const raw = value.trim(); if (!raw || raw.includes("\n")) return false; if (/^[a-z][a-z0-9+.-]*:\/\//i.test(raw)) return false; + if (isFilePatternReference(raw)) return false; if (!/[\\/]/.test(raw) && !/^(dockerfile|makefile|readme|package-lock\.json)$/i.test(raw)) { return false; } @@ -137,6 +138,10 @@ export function isLikelyFilePath(value: string): boolean { return /\.[a-z0-9][a-z0-9_-]{0,12}$/i.test(name); } +export function isFilePatternReference(value: string): boolean { + return /[*?[\]{}]/.test(value.trim()); +} + export function splitFilePath(path: string): { directory: string; name: string } { const normalized = path.replace(/\\/g, "/"); const slash = normalized.lastIndexOf("/"); diff --git a/webui/src/components/MarkdownTextRenderer.tsx b/webui/src/components/MarkdownTextRenderer.tsx index 0461dec9d..307a45f16 100644 --- a/webui/src/components/MarkdownTextRenderer.tsx +++ b/webui/src/components/MarkdownTextRenderer.tsx @@ -17,7 +17,11 @@ import remarkMath from "remark-math"; import { AttachmentTile } from "@/components/AttachmentTile"; import { CodeBlock } from "@/components/CodeBlock"; -import { FileReferenceChip, isLikelyFilePath } from "@/components/FileReferenceChip"; +import { + FileReferenceChip, + isFilePatternReference, + isLikelyFilePath, +} from "@/components/FileReferenceChip"; import { inferMediaKind } from "@/lib/media"; import { faviconUrls } from "@/lib/provider-brand"; import { cn } from "@/lib/utils"; @@ -216,12 +220,19 @@ function cleanFileReferenceTarget(value: string): string { } function isPreviewableFileTarget(value: string): boolean { + if (isFilePatternReference(value)) return false; if (isLikelyFilePath(value)) return true; if (/^[a-z][a-z0-9+.-]*:\/\//i.test(value)) return false; if (/[\\/]/.test(value)) return false; return /^[^?#]+\.[a-z0-9][a-z0-9_-]{0,12}$/i.test(value); } +function isNonNavigableFilePatternLink(href: string | undefined): boolean { + if (!href || /^https?:\/\//i.test(href) || href.startsWith("#")) return false; + const target = cleanFileReferenceTarget(href); + return Boolean(target && isFilePatternReference(target)); +} + function fileReferenceFromLink(href: string | undefined): string | null { if (!href || /^https?:\/\//i.test(href) || href.startsWith("#")) return null; const target = cleanFileReferenceTarget(href); @@ -469,6 +480,9 @@ export default function MarkdownTextRenderer({ /> ); } + if (isNonNavigableFilePatternLink(href)) { + return <>{markdownChildren}; + } return ( { ); }); + it("renders glob file links as plain text instead of preview targets", () => { + const onOpenFilePreview = vi.fn(); + const { container } = render( + + {"原始对话通常还在 [*.json](*.json)。"} + , + ); + + expect(screen.queryByTestId("inline-file-path")).not.toBeInTheDocument(); + expect(screen.queryByRole("link", { name: "*.json" })).not.toBeInTheDocument(); + expect(container).toHaveTextContent("*.json"); + }); + + it("keeps glob inline code as code instead of a file preview chip", () => { + render( + + {"检查 `src/**/*.json`。"} + , + ); + + expect(screen.queryByTestId("inline-file-path")).not.toBeInTheDocument(); + expect(screen.getByText("src/**/*.json").tagName).toBe("CODE"); + }); + it("does not wrap complete fenced code blocks in an extra pre", () => { const { container } = render(