diff --git a/packages/client/ui-primitives/README.i18n.yaml b/packages/client/ui-primitives/README.i18n.yaml index 74c0cf2516..d6c7014509 100644 --- a/packages/client/ui-primitives/README.i18n.yaml +++ b/packages/client/ui-primitives/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/ui-primitives/README.md -README.md: 29d7b5e20dbc3a53fe4b85e219b0c957c05ed873 -README.zh.md: 92d256f7f1d413a9792aa54f50317c2a15dd09a0 +README.md: 99d5f8b238ce78d83de0b5247d194b308dd6fac0 +README.zh.md: 468aa75e88580d6ed5e78666ad34c18b22a85dd3 diff --git a/packages/client/ui-primitives/README.md b/packages/client/ui-primitives/README.md index 29d7b5e20d..99d5f8b238 100644 --- a/packages/client/ui-primitives/README.md +++ b/packages/client/ui-primitives/README.md @@ -6,7 +6,7 @@ Pure React atoms (zero cordis): StateDot, ic_ds_* icons, Button/Pill/Menu/Modal/ ## Markdown rendering -`MarkdownText` renders GFM from untrusted assistant output through React elements. It omits raw HTML, neutralizes relative and non-HTTP(S)/mailto links, opens HTTP(S) links with safe external-link attributes, and renders image alt text without loading remote resources; `MessageText` remains the literal-text primitive for user-authored content. Element spacing, tables, links, and inline code use the same `--dsw-alias-markdown-*` / `--dsw-font-markdown-*` tokens as deepsuite `@deepseek/md`. Fenced blocks render through `CodeBlock` (language banner, copy control, shiki for the registered grammars). +`MarkdownText` renders GFM from untrusted assistant output through React elements. It omits raw HTML, neutralizes relative and non-HTTP(S)/mailto links, opens HTTP(S) links with safe external-link attributes, and renders image alt text without loading remote resources; `MessageText` remains the literal-text primitive for user-authored content. `extractMarkdownPlainText` removes Markdown presentation markup for compact labels while preserving raw HTML as literal text. Element spacing, tables, links, and inline code use the same `--dsw-alias-markdown-*` / `--dsw-font-markdown-*` tokens as deepsuite `@deepseek/md`. Fenced blocks render through `CodeBlock` (language banner, copy control, shiki for the registered grammars). ## Model Experience diff --git a/packages/client/ui-primitives/README.zh.md b/packages/client/ui-primitives/README.zh.md index 92d256f7f1..468aa75e88 100644 --- a/packages/client/ui-primitives/README.zh.md +++ b/packages/client/ui-primitives/README.zh.md @@ -6,7 +6,7 @@ ## Markdown 渲染 -`MarkdownText` 通过 React 元素渲染来自不受信任 assistant 输出的 GFM。它会省略原始 HTML,使相对链接及非 HTTP(S)/mailto 链接失效,以安全的外部链接属性打开 HTTP(S) 链接,并只渲染图片 alt 文本而不加载远程资源;`MessageText` 仍是用户创作内容使用的字面文本原语。元素间距、表格、链接与行内代码使用与 deepsuite `@deepseek/md` 相同的 `--dsw-alias-markdown-*` / `--dsw-font-markdown-*` token。围栏代码块通过 `CodeBlock` 渲染(语言横幅、复制控件,以及对已注册语法使用 shiki)。 +`MarkdownText` 通过 React 元素渲染来自不受信任 assistant 输出的 GFM。它会省略原始 HTML,使相对链接及非 HTTP(S)/mailto 链接失效,以安全的外部链接属性打开 HTTP(S) 链接,并只渲染图片 alt 文本而不加载远程资源;`MessageText` 仍是用户创作内容使用的字面文本原语。`extractMarkdownPlainText` 会移除 Markdown 呈现标记以用于紧凑标签,同时将原始 HTML 保留为字面文本。元素间距、表格、链接与行内代码使用与 deepsuite `@deepseek/md` 相同的 `--dsw-alias-markdown-*` / `--dsw-font-markdown-*` token。围栏代码块通过 `CodeBlock` 渲染(语言横幅、复制控件,以及对已注册语法使用 shiki)。 ## 模型体验 diff --git a/packages/client/ui-primitives/src/markdown/plain-text.ts b/packages/client/ui-primitives/src/markdown/plain-text.ts index a3e20c1179..af449dc774 100644 --- a/packages/client/ui-primitives/src/markdown/plain-text.ts +++ b/packages/client/ui-primitives/src/markdown/plain-text.ts @@ -1,6 +1,6 @@ /** * Markdown-to-plain-text projection for compact summaries and labels. - * Parsing shares the renderer's GFM grammar; raw HTML is omitted, links + * Parsing shares the renderer's GFM grammar; raw HTML stays literal, links * keep their labels, images keep alt text, and code keeps its source text. */ @@ -36,7 +36,7 @@ function inlineText(node: MarkdownNode): string { case 'break': return '\n' case 'html': - return '' + return node.value ?? '' default: return node.children?.map(inlineText).join('') ?? '' } @@ -67,7 +67,7 @@ function blockText(node: MarkdownNode): string { case 'tableCell': return compactInline(inlineText(node)) case 'html': - return '' + return node.value ?? '' case 'thematicBreak': case 'definition': return '' @@ -98,7 +98,7 @@ function fullText(root: MarkdownNode): string { } /** - * Parse GFM Markdown and project its user-visible plain text. + * Parse GFM Markdown, remove its presentation markup, and preserve raw HTML literally. * @param markdown - Markdown source. * @param options - Optional extraction boundary. * @returns Plain text for the whole document, first visible line, or first semantic paragraph. diff --git a/packages/client/ui-primitives/tests/markdown-plain-text.spec.ts b/packages/client/ui-primitives/tests/markdown-plain-text.spec.ts index 481d78bd8a..010d5803ab 100644 --- a/packages/client/ui-primitives/tests/markdown-plain-text.spec.ts +++ b/packages/client/ui-primitives/tests/markdown-plain-text.spec.ts @@ -34,10 +34,18 @@ describe('extractMarkdownPlainText', () => { .toBe('First paragraph with a link and diagram.') }) - it('omits raw HTML and returns a useful fallback when no paragraph exists', () => { - const markdown = '\n\n## Safe heading' - expect(extractMarkdownPlainText(markdown)).toBe('Safe heading') - expect(extractMarkdownPlainText(markdown, { mode: 'first-paragraph' })).toBe('Safe heading') + it('preserves raw HTML while removing Markdown presentation markup', () => { + const block = [ + '', + 'Command: pnpm test', + 'Exit code: 0', + '', + ].join('\n') + expect(extractMarkdownPlainText(block)).toBe(block) + expect(extractMarkdownPlainText('**Status:** ready')) + .toBe('Status: ready') + expect(extractMarkdownPlainText(block, { mode: 'first-paragraph' })) + .toBe('') }) it('projects GFM tables, references, hard breaks, and block structure', () => { @@ -54,7 +62,7 @@ describe('extractMarkdownPlainText', () => { '[asset]: diagram.png', ].join('\n') expect(extractMarkdownPlainText(markdown)).toBe([ - 'first second with diagram and visible', + 'first second with diagram and visible', '', 'Name\tValue', 'alpha\t1', diff --git a/packages/client/ui-trajectory/tests/table.spec.tsx b/packages/client/ui-trajectory/tests/table.spec.tsx index ee073c754e..7c0b567311 100644 --- a/packages/client/ui-trajectory/tests/table.spec.tsx +++ b/packages/client/ui-trajectory/tests/table.spec.tsx @@ -83,6 +83,34 @@ describe('TrajectoryTable', () => { expect(screen.getByText('15 tok')).toBeTruthy() }) + it('keeps raw HTML tags in a Markdown-derived context preview', () => { + const html = [ + '', + 'Command: pnpm test', + 'Exit code: 0', + '', + ].join('\n') + const turns: readonly TrajectoryTurnModel[] = [{ + turn: 1, + groups: [{ + title: 'Message', + cells: [{ + index: 1, + kind: 'context', + text: '', + inputDetail: html, + timeSeconds: 0, + }], + }], + }] + + render() + + expect(screen.getByText( + ' Command: pnpm test Exit code: 0 ', + )).toBeTruthy() + }) + it('keeps running and failure semantics distinct from record roles', () => { const view = render() expect(view.container.querySelector('tr[data-kind="tool"][data-running="true"]')).toBeTruthy()