mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-09-14 04:01:35 +00:00
fix(ui): retain Mermaid previews and support hybrid touch controls
This commit is contained in:
@@ -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 .agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.md
|
||||
2026-09-07-web-mermaid-preview.md: df50e7c7f8b78a869d7d5b761864c06648f8d064
|
||||
2026-09-07-web-mermaid-preview.zh.md: 282e6fb4365a56425f2b341e2367e4a2cdae5c1c
|
||||
2026-09-07-web-mermaid-preview.md: 811f688713491333587404d32946317d81ff2795
|
||||
2026-09-07-web-mermaid-preview.zh.md: 0fdb9cf318561f20fdb7042121bc3517c4d6c9a0
|
||||
|
||||
@@ -10,7 +10,7 @@ Assistant replies can describe diagrams in Mermaid code fences, but readers must
|
||||
|
||||
## Decision
|
||||
|
||||
Chat enables settled fence previews through `MarkdownLabels.mermaid`. The shared Markdown renderer uses the parsed fence language; streaming fences and consumers without those labels retain code display. The static [UI primitives package](../../../../packages/client/ui-primitives/README.md) owns `MermaidPreview`, which accepts source and localized labels without Session, file, or Cordis dependencies. `CodeBlock.preview` owns view switching and source copying. Previews omit the language banner and expose compact icon actions on hover or keyboard focus; touch devices keep those actions visible. Source and preview share the same focused toggle button, and copying always reads the source prop.
|
||||
Chat enables settled fence previews through `MarkdownLabels.mermaid`. The shared Markdown renderer uses the parsed fence language; streaming fences and consumers without those labels retain code display. The static [UI primitives package](../../../../packages/client/ui-primitives/README.md) owns `MermaidPreview`, which accepts source and localized labels without Session, file, or Cordis dependencies. `CodeBlock.preview` owns view switching and source copying. Previews omit the language banner and expose compact icon actions on hover or keyboard focus; devices with any touch input keep those actions visible below the diagram. Switching to source hides the mounted preview, so returning preserves completed rendering and pending work. Source and preview share the same focused toggle button, and copying always reads the source prop.
|
||||
|
||||
Mermaid loads on demand. Its public render API serializes diagram work, and each call removes its temporary measurement DOM in `finally`. Strict security, disabled HTML labels, the neutral theme, and error-rendering policy cannot be overridden by diagram configuration. Generated SVG is displayed as an image without installing diagram links or scripts. Intrinsic dimensions come from the SVG viewBox; large diagrams shrink to fit, and the canvas stays light in both application themes.
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ Assistant 回复可以在 Mermaid 代码 fence 中描述图表,但读者必须
|
||||
|
||||
## 决策
|
||||
|
||||
Chat 通过 `MarkdownLabels.mermaid` 启用已定稿 fence 预览。共享 Markdown 渲染器使用解析后的 fence 语言;流式 fence 和未传入这些 label 的调用方保留代码显示。静态 [UI primitives 包](../../../../packages/client/ui-primitives/README.zh.md)拥有 `MermaidPreview`,它接收源码与本地化 label,不依赖 Session、文件或 Cordis。`CodeBlock.preview` 拥有视图切换与源码复制。预览省略语言标题栏,悬停或键盘聚焦时显示紧凑的图标操作;触屏上保持操作可见。源码与预览使用同一个已聚焦的切换按钮,复制始终读取源码 prop。
|
||||
Chat 通过 `MarkdownLabels.mermaid` 启用已定稿 fence 预览。共享 Markdown 渲染器使用解析后的 fence 语言;流式 fence 和未传入这些 label 的调用方保留代码显示。静态 [UI primitives 包](../../../../packages/client/ui-primitives/README.zh.md)拥有 `MermaidPreview`,它接收源码与本地化 label,不依赖 Session、文件或 Cordis。`CodeBlock.preview` 拥有视图切换与源码复制。预览省略语言标题栏,悬停或键盘聚焦时显示紧凑的图标操作;具备任何触控输入的设备均在图表下方保持操作可见。切换到源码时隐藏已挂载的预览,因此返回时保留已完成的渲染与进行中的工作。源码与预览使用同一个已聚焦的切换按钮,复制始终读取源码 prop。
|
||||
|
||||
Mermaid 按需加载。它的公开 render API 串行执行图表工作,每次调用在 `finally` 中移除临时测量 DOM。图表配置无法覆盖严格安全模式、禁用 HTML label、中性主题和错误渲染策略。生成的 SVG 以图片显示,不安装图内链接或脚本。固有尺寸取自 SVG viewBox;大图缩小以适应宽度,画布在两种应用主题下均保持浅色。
|
||||
|
||||
|
||||
@@ -94,6 +94,7 @@ describe('web e2e: Mermaid chat previews', () => {
|
||||
expect(await page.locator('pre code').allTextContents()).toContain(INVALID)
|
||||
expect(await images.first().evaluate(node => decodeURIComponent((node as HTMLImageElement).src))).toContain('共享渲染器')
|
||||
const first = page.locator('.md-code-block').first()
|
||||
const diagram = await first.getByRole('img').elementHandle()
|
||||
const controls = first.locator('[class*="bannerWrap"]')
|
||||
expect(await first.getByText('mermaid', { exact: true }).count()).toBe(0)
|
||||
await page.mouse.move(0, 0)
|
||||
@@ -109,8 +110,10 @@ describe('web e2e: Mermaid chat previews', () => {
|
||||
expect(await first.getByRole('button', { name: 'Preview', exact: true })
|
||||
.evaluate(node => node === document.activeElement)).toBe(true)
|
||||
expect(await first.locator('pre code').textContent()).toBe(FLOW)
|
||||
expect(await diagram!.evaluate(node => node.isConnected)).toBe(true)
|
||||
expect(await diagram!.isVisible()).toBe(false)
|
||||
await first.getByRole('button', { name: 'Preview', exact: true }).click()
|
||||
await first.getByRole('img').waitFor()
|
||||
expect(await diagram!.isVisible()).toBe(true)
|
||||
await first.getByRole('button', { name: 'Copy', exact: true }).waitFor()
|
||||
expect(await page.locator('body').evaluate(node => getComputedStyle(node).display)).not.toBe('none')
|
||||
expect(await page.locator('[id^="dsh-mermaid-"]').count()).toBe(0)
|
||||
@@ -145,4 +148,31 @@ describe('web e2e: Mermaid chat previews', () => {
|
||||
await assertFixtureInventory(SNAPSHOT_DIR, ['ui.expected.md', 'zh.expected.md'])
|
||||
await page.close()
|
||||
}, 60_000)
|
||||
|
||||
it.skipIf(MODE === 'record')('keeps actions visible when a mouse and touchscreen are both available', async () => {
|
||||
const hybridBrowser = await chromium.launch({
|
||||
args: ['--blink-settings=availablePointerTypes=6,primaryPointerType=4,availableHoverTypes=2,primaryHoverType=2'],
|
||||
})
|
||||
try {
|
||||
const page = await newEnglishPage(hybridBrowser)
|
||||
expect(await page.evaluate(() =>
|
||||
matchMedia('(hover: hover) and (pointer: fine) and (any-pointer: coarse)').matches)).toBe(true)
|
||||
await openConversation(page, scaffold)
|
||||
const first = page.locator('.md-code-block').first()
|
||||
const diagram = first.getByRole('img', { name: 'Mermaid diagram' })
|
||||
await expect.poll(() => diagram.evaluate(node => (node as HTMLImageElement).naturalWidth)).toBeGreaterThan(0)
|
||||
await page.mouse.move(0, 0)
|
||||
const controls = first.locator('[class*="bannerWrap"]')
|
||||
expect(await controls.evaluate(node => getComputedStyle(node).opacity)).toBe('1')
|
||||
expect(await controls.evaluate(node => getComputedStyle(node).pointerEvents)).toBe('auto')
|
||||
expect(await first.getByRole('button', { name: 'Source', exact: true })
|
||||
.evaluate(node => node.getBoundingClientRect().width)).toBe(44)
|
||||
expect(await controls.evaluate(node => node.getBoundingClientRect().top))
|
||||
.toBeGreaterThanOrEqual(await diagram.evaluate(node => node.getBoundingClientRect().bottom))
|
||||
await first.getByRole('button', { name: 'Source', exact: true }).click()
|
||||
expect(await first.locator('pre code').textContent()).toBe(FLOW)
|
||||
} finally {
|
||||
await hybridBrowser.close()
|
||||
}
|
||||
}, 60_000)
|
||||
})
|
||||
|
||||
@@ -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: f2694588e2ce96f10055802adaf5ff23b9cae9f1
|
||||
README.zh.md: cbd7fec0c1708b9d902dff2302c9472ed2bbb56b
|
||||
README.md: 0ba4d6baaa54bbacc82202c490423b37613874df
|
||||
README.zh.md: 06a266b61a79d4ff5e38ca0de9877a6d8ec66af0
|
||||
|
||||
@@ -76,7 +76,7 @@ The catalog above lists what each export is for; this section covers the behavio
|
||||
|
||||
`MermaidPreview` renders complete Mermaid source on a light canvas. Rendering loads Mermaid on demand, uses strict security, and exposes the generated SVG as an image with no diagram link handlers. A render failure shows the original source with the supplied error label; replacing the source discards late results from the previous render. The diagram keeps its intrinsic size and shrinks to fit the available width.
|
||||
|
||||
Supply `MarkdownLabels.mermaid` to enable `mermaid` fence previews in a Markdown consumer; without it, fences remain code. Previews start after the message settles. `CodeBlock.preview` supplies an optional headerless body with icon actions for source switching and copying. Actions appear on hover or keyboard focus, and stay visible on touch devices; copying always retains the source. [The Mermaid decision](../../../.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.md) records the rendering and reuse choices.
|
||||
Supply `MarkdownLabels.mermaid` to enable `mermaid` fence previews in a Markdown consumer; without it, fences remain code. Previews start after the message settles. `CodeBlock.preview` supplies an optional headerless body with icon actions for source switching and copying. Actions appear on hover or keyboard focus, and stay visible below the diagram whenever a touchscreen is available, including with a mouse attached. Switching to source keeps the preview mounted, so returning reuses its result; copying always retains the source. [The Mermaid decision](../../../.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.md) records the rendering and reuse choices.
|
||||
|
||||
### Localizing copy
|
||||
|
||||
@@ -145,6 +145,7 @@ None; this package neither assembles nor sends a provider request.
|
||||
|
||||
These limits define how the atoms behave at the edges; they are current package constraints, not a component roadmap.
|
||||
|
||||
- **Mermaid rendering runs on the browser thread** — every mounted settled preview starts rendering, including off-screen diagrams. Mermaid serializes layouts; work already submitted to it cannot be interrupted. Preview virtualization and worker rendering are not provided.
|
||||
- **Streaming defers cross-boundary reference resolution** — a reference-style link or footnote whose definition sits on the other side of the incremental freeze boundary renders as literal text while the reply streams; the settled full parse at finalize resolves it.
|
||||
- **A long highlighted fence retains its complete token DOM** — streaming avoids re-parsing, re-tokenizing, and reconciling the completed prefix, but it does not discard old colors or virtualize token spans. Final DOM cardinality therefore still follows the fence's token count; nested/container fences and a pathological single long line remain on the general tail path.
|
||||
- **Glyph-level icons are redrawn approximations** — the fish logo and the sparkle mark come from font glyphs whose vector geometry is not exportable from the local design data; hand-authored recreations stand in until an exact export path exists.
|
||||
|
||||
@@ -76,7 +76,7 @@ kind: "package-library"
|
||||
|
||||
`MermaidPreview` 在浅色画布上渲染完整的 Mermaid 源码。渲染按需加载 Mermaid、使用严格安全模式,并把生成的 SVG 显示为图片,不绑定图内链接处理器。渲染失败时显示原始源码与传入的错误文案;替换源码后会丢弃前一次渲染的延迟结果。图表保留固有尺寸,并在可用宽度不足时缩小。
|
||||
|
||||
为 Markdown 调用方传入 `MarkdownLabels.mermaid` 即可启用 `mermaid` fence 预览;未传入时,fence 保持代码显示。预览在消息定稿后开始。`CodeBlock.preview` 提供无标题栏的替代正文,以及切换源码和复制的图标操作。操作在悬停或键盘聚焦时出现,触屏上保持可见;复制始终保留源码。[Mermaid 决策](../../../.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.zh.md)记录了渲染与复用选择。
|
||||
为 Markdown 调用方传入 `MarkdownLabels.mermaid` 即可启用 `mermaid` fence 预览;未传入时,fence 保持代码显示。预览在消息定稿后开始。`CodeBlock.preview` 提供无标题栏的替代正文,以及切换源码和复制的图标操作。操作在悬停或键盘聚焦时出现;只要设备具备触屏,就在图表下方保持可见,包括同时连接鼠标的情况。切换到源码时保留已挂载的预览,返回时复用其结果;复制始终保留源码。[Mermaid 决策](../../../.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.zh.md)记录了渲染与复用选择。
|
||||
|
||||
### 本地化文案
|
||||
|
||||
@@ -145,6 +145,7 @@ kind: "package-library"
|
||||
|
||||
这些限制说明原子组件在边缘情况下的行为;它们是当前包约束,不是组件路线图。
|
||||
|
||||
- **Mermaid 渲染在浏览器线程上执行**:每个已挂载的定稿预览都会开始渲染,包括视口外的图表。Mermaid 串行执行布局,已提交给它的工作无法中断。当前不提供预览虚拟化或 worker 渲染。
|
||||
- **流式期间跨边界引用解析被推迟**:定义落在增量冻结边界另一侧的引用式链接或脚注,在回复流式输出期间渲染为字面文本;定稿时的全量解析会将其解析。
|
||||
- **长高亮 fence 会保留完整 token DOM**:流式路径避免重新解析、重新 tokenize 和 reconcile 已完成前缀,但不会丢弃旧颜色或虚拟化 token span。因此最终 DOM 数量仍随 fence 的 token 数增长;嵌套/容器内 fence 与病态的单个超长行仍走通用尾部路径。
|
||||
- **字形级图标是重新绘制的近似版本**:鱼形标志与闪光标记来自字体字形,而本地设计数据无法导出其矢量几何;在获得精确导出路径前,使用手工重建版本代替。
|
||||
|
||||
@@ -184,7 +184,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (pointer: coarse) {
|
||||
@media (any-pointer: coarse) {
|
||||
.preview {
|
||||
padding-bottom: 56px;
|
||||
}
|
||||
@@ -192,6 +192,8 @@
|
||||
.preview .bannerWrap {
|
||||
top: auto;
|
||||
bottom: 8px;
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.preview .iconButton {
|
||||
|
||||
@@ -217,7 +217,8 @@ export function CodeBlock({ code, lang, streaming, className, lineNumbers = fals
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{showingPreview ? preview.content : body}
|
||||
{previewAvailable && <div hidden={!showingPreview}>{preview.content}</div>}
|
||||
{!showingPreview && body}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ export interface MarkdownCodeLabels {
|
||||
export interface MarkdownLabels {
|
||||
code: MarkdownCodeLabels
|
||||
footnotes: string
|
||||
/** Opt into settled Mermaid fence previews by supplying their complete localized chrome. */
|
||||
/** Opt into settled Mermaid fence previews by supplying their complete localized labels. */
|
||||
mermaid?: MermaidPreviewLabels & { preview: string; source: string }
|
||||
}
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ describe('Markdown Mermaid fences', () => {
|
||||
expect(renderMermaid).not.toHaveBeenCalled()
|
||||
expect(screen.queryByRole('button', { name: labels.source })).toBeNull()
|
||||
view.rerender(<MarkdownText {...props} />)
|
||||
await screen.findByRole('img', { name: labels.diagram })
|
||||
const diagram = await screen.findByRole('img', { name: labels.diagram })
|
||||
expect(screen.queryByText('mermaid')).toBeNull()
|
||||
expect(screen.getByRole('button', { name: labels.source }).textContent).toBe('')
|
||||
expect(screen.getByRole('button', { name: markdownLabels.code.copyLabel }).textContent).toBe('')
|
||||
@@ -96,9 +96,12 @@ describe('Markdown Mermaid fences', () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: labels.source }))
|
||||
expect(view.container.querySelector('pre code')?.textContent).toBe(source)
|
||||
expect(screen.queryByRole('img')).toBeNull()
|
||||
expect(diagram.isConnected).toBe(true)
|
||||
expect(screen.queryByText('mermaid')).toBeNull()
|
||||
fireEvent.click(screen.getByRole('button', { name: labels.preview }))
|
||||
await screen.findByRole('img', { name: labels.diagram })
|
||||
expect(screen.getByRole('img', { name: labels.diagram })).toBe(diagram)
|
||||
expect(screen.queryByRole('status')).toBeNull()
|
||||
expect(renderMermaid).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('leaves other languages and consumers without preview labels as code', () => {
|
||||
|
||||
Reference in New Issue
Block a user