From 228deb738fa97cdbcd72164b7b33f1b11740bdec Mon Sep 17 00:00:00 2001 From: kangkang Date: Sat, 12 Sep 2026 21:53:00 +0800 Subject: [PATCH] =?UTF-8?q?fix(ui):=20=E4=BF=AE=E5=A4=8D=E6=97=A0=E8=AF=AD?= =?UTF-8?q?=E8=A8=80=20fenced=20code=20=E7=9A=84=E5=9D=97=E7=BA=A7?= =?UTF-8?q?=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Related to #28. 使用 pre 上下文区分块级代码与行内代码,避免无 language-* 标记的 fenced block 被渲染成行内样式,并补充回归测试。 --- .../components/chat/MarkdownContent.test.tsx | 13 ++++++ .../src/components/chat/MarkdownContent.tsx | 41 +++++++++++-------- 2 files changed, 36 insertions(+), 18 deletions(-) diff --git a/packages/ui/src/components/chat/MarkdownContent.test.tsx b/packages/ui/src/components/chat/MarkdownContent.test.tsx index 9b5b3fc..c1a5654 100644 --- a/packages/ui/src/components/chat/MarkdownContent.test.tsx +++ b/packages/ui/src/components/chat/MarkdownContent.test.tsx @@ -34,6 +34,19 @@ describe('MarkdownContent', () => { expect(container.querySelector('pre code')?.textContent).toContain('{"ok":true}'); }); + it('renders untyped fenced code as a block rather than inline code', async () => { + const container = document.createElement('div'); + const root = createRoot(container); + + await act(async () => { + root.render(); + }); + + expect(container.querySelector('pre code')?.className).toContain('font-mono'); + expect(container.querySelector('pre code')?.className).not.toContain('rounded-[4px]'); + expect(container.querySelector('p code')?.className).toContain('rounded-[4px]'); + }); + it('renders GFM tables and removes unsafe links', async () => { const container = document.createElement('div'); const root = createRoot(container); diff --git a/packages/ui/src/components/chat/MarkdownContent.tsx b/packages/ui/src/components/chat/MarkdownContent.tsx index 0c01317..07e9613 100644 --- a/packages/ui/src/components/chat/MarkdownContent.tsx +++ b/packages/ui/src/components/chat/MarkdownContent.tsx @@ -3,6 +3,7 @@ import ReactMarkdown, { type Components } from 'react-markdown'; import remarkGfm from 'remark-gfm'; const SAFE_URL_PROTOCOLS = new Set(['http:', 'https:', 'mailto:', 'tel:']); +const CodeBlockContext = React.createContext(false); /** * Keep agent-authored URLs useful without allowing navigation to executable @@ -79,6 +80,22 @@ function useStreamingContent(content: string, streaming: boolean): string { * is intentional. GFM adds the table, task-list, strike-through, and URL * behaviours people expect from a research answer. */ +const MarkdownCode: NonNullable = ({ className, children, ...props }) => { + const isBlock = React.useContext(CodeBlockContext) || Boolean(className?.includes('language-')); + return ( + + {isBlock ? String(children).replace(/\n$/, '') : children} + + ); +}; + const components: Components = { h1: ({ children }) => (

{children}

@@ -116,25 +133,13 @@ const components: Components = { ); }, - code: ({ className, children, ...props }) => { - const isBlock = Boolean(className?.includes('language-')); - return ( - - {isBlock ? String(children).replace(/\n$/, '') : children} - - ); - }, + code: MarkdownCode, pre: ({ children }) => ( -
-      {children}
-    
+ +
+        {children}
+      
+
), table: ({ children }) => (