diff --git a/webui/src/components/MarkdownTextRenderer.tsx b/webui/src/components/MarkdownTextRenderer.tsx index ff75004a7..aa757ff00 100644 --- a/webui/src/components/MarkdownTextRenderer.tsx +++ b/webui/src/components/MarkdownTextRenderer.tsx @@ -6,6 +6,7 @@ import remarkGfm from "remark-gfm"; import remarkMath from "remark-math"; import { CodeBlock } from "@/components/CodeBlock"; +import { FileReferenceChip, isLikelyFilePath } from "@/components/FileReferenceChip"; import { cn } from "@/lib/utils"; import "katex/dist/katex.min.css"; @@ -44,6 +45,9 @@ export default function MarkdownTextRenderer({ ); } const raw = String(kids).replace(/\n$/, ""); + if (isLikelyFilePath(raw)) { + return ; + } /** Plain fenced ``` blocks (no language) & wide one-liners: block monospace, not inline pill. */ const widePlainBlock = raw.includes("\n") || raw.length > 120; if (widePlainBlock) { diff --git a/webui/src/components/ui/tooltip.tsx b/webui/src/components/ui/tooltip.tsx index 95f7960c8..d69f12275 100644 --- a/webui/src/components/ui/tooltip.tsx +++ b/webui/src/components/ui/tooltip.tsx @@ -11,15 +11,17 @@ const TooltipContent = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, sideOffset = 4, ...props }, ref) => ( - + + + )); TooltipContent.displayName = TooltipPrimitive.Content.displayName; diff --git a/webui/src/tests/message-bubble.test.tsx b/webui/src/tests/message-bubble.test.tsx index 410fbabaf..baae344dc 100644 --- a/webui/src/tests/message-bubble.test.tsx +++ b/webui/src/tests/message-bubble.test.tsx @@ -1,4 +1,4 @@ -import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { act, fireEvent, render, screen, waitFor } from "@testing-library/react"; import { describe, expect, it, vi } from "vitest"; import { MessageBubble } from "@/components/MessageBubble"; @@ -179,6 +179,47 @@ describe("MessageBubble", () => { expect(screen.getByText("Body line.")).toBeInTheDocument(); }); + it("renders inline file paths as compact file references", async () => { + await import("@/components/MarkdownTextRenderer"); + const message: UIMessage = { + id: "a-file-path", + role: "assistant", + content: + "改动在 `webui/src/components/MarkdownTextRenderer.tsx` 和 `/Users/renxubin/.nanobot/workspace/minecraft-fps/index.html`。", + createdAt: Date.now(), + }; + + try { + render(); + + const references = await screen.findAllByTestId("inline-file-path"); + expect(references).toHaveLength(2); + expect(references[0].parentElement).not.toHaveClass("translate-y-[0.08em]"); + expect(references[0].parentElement).toHaveClass("align-[0.14em]"); + expect(references[0]).toHaveTextContent("MarkdownTextRenderer.tsx"); + expect(references[0]).not.toHaveTextContent("webui/src/components"); + expect(screen.getByText("index.html")).toBeInTheDocument(); + expect(references[1]).not.toHaveTextContent("/Users/renxubin"); + expect(references[1]).not.toHaveAttribute("title"); + expect(references[1]).toHaveAttribute( + "aria-label", + "/Users/renxubin/.nanobot/workspace/minecraft-fps/index.html", + ); + + vi.useFakeTimers(); + fireEvent.pointerMove(references[1].parentElement!); + await act(async () => { + vi.advanceTimersByTime(500); + }); + const tooltip = screen.getByRole("tooltip"); + expect(tooltip).toHaveTextContent( + "/Users/renxubin/.nanobot/workspace/minecraft-fps/index.html", + ); + } finally { + vi.useRealTimers(); + } + }); + it("renders assistant image media as a larger generated result", () => { const message: UIMessage = { id: "a-image",