Repository navigation
feat(webui): render Mermaid fences in the transcript - #125
stevenjj33 wants to merge 3 commits into
Conversation
Closes the acceptance table''s "markdown-图表(mmd)" row: the table said "通过" while the source had zero Mermaid support, so a model-drawn flowchart or sequence diagram reached the transcript as plain text. The fence branch sits in `markdown.tsx` between the math and generic code branches and fires only for the `mermaid`/`mmd` language words — `codeFenceLanguage` already strips tag attributes, so a fence tagged `mermaid title="a.mmd"` is recognised, and the match is case-insensitive. Every other fence keeps the untouched path, which C-1 and C-2 pin byte for byte. Rendering lives in the new `webui-mermaid.tsx`. The library loads through a dynamic `import()` on the first fence that needs it, and invalid syntax, an empty body, a thrown render and a render past the timeout all fall back to the plain fence — the same failure habit as katex''s `<pre>` fallback, minus the gutter a Mermaid fence never had. The component is split into a pure block and a thin effect wrapper so the Node-environment suite can drive each state without a DOM, the pattern `ReviewSettingsPage.tsx` already uses. Build: the client bundle is now split (`splitting: true`, chunks under `assets/`). Without splitting esbuild inlines every dynamic import into the entry — mermaid''s source took `client.js` from 4.11 MB to 15.35 MB. Measured on the metafile, the initial payload is unchanged at 4.11 MB and the ~11 MB of diagram chunks download only when a fence appears. `test/webui-browser/server.mjs` serves the `assets/` prefix, matching the loopback service''s static resolver. `webui-dependency-rules.mjs` classifies the new file as `bindings`, its siblings'' layer, so the direction gate stays green. Verification: `markdown-mermaid.test.tsx` (18 cases) and the C-1/C-2 suites pass, typecheck:client and typecheck:test report 0 errors, and the browser suite renders a real `<svg>` — removing the branch turns that case red, which is the reverse check the task book asks for. On Windows, 13 pre-existing suite failures (path-with-space normalisation, symlink privilege, websocket shutdown timing) fail identically on the base commit.
e40a63f moved composer-store ownership into the application and made the shell adopt the composition root's application (A4-s2). The root built its application without a `composer` dep, so `create-application` fell back to `createWebuiComposerStore()` — storage-less by design, because the application layer may not import infrastructure. The shell's own storage-backed store (`providedApplication?.composer ?? create(...)`) became dead code on the live path, and with it every draft: typing still worked and switching sessions still restored the in-memory draft, but nothing ever reached localStorage, so a reload lost them all. Verified on webui @ 6a455a2 before this change: the drafts spec failed deterministically (3 runs, same failure — after reload the composer is empty and `webui.composer.state.v1` is null even before the reload), and a probe showed the key was never written. With the injection: 7/7 in composer.spec.mjs, 97/97 in the full browser suite. The composition root now injects the same storage adapter the shell would have built, exactly as the shell already does for `unreadStorage` when it builds its own application. The unread controller has the same gap at the same call site (no failing gate covers it); left for a follow-up rather than widening this fix.
6439875 to
1939394
Compare
本地修复记录(拉分支跑通到全绿,未重写功能)按任务书「拉到本地跑通构建与测试,先修到绿再提评审」执行。功能提交本身一字节未改(冲突均为机械路径适配),以下四项是让它从「CI 从没跑过」到「本地全部门禁绿」的实际修复:
本地全量结果: 产物大小更正(对当前 webui HEAD 实测,非旧基点)原文表格的数字取自旧基点,对
入口不增反降是因为本分支给构建加了 CI 状态说明仓库级 Actions 当前被关闭( |
modacker
left a comment
There was a problem hiding this comment.
这条 PR 在当前 webui(6b3b8183)上重新核了一遍。功能本身在 base 上还不存在(git grep -il mermaid origin/webui -- packages/webui scripts test release 只命中文档和一个会话标题字符串测试),所以它不过时;但结论是关掉重提,理由是它绑定的是 2026-10 的旧目录结构和已经被重构的浏览器测试基础设施,加上几处自身的缺陷。
路径全要重定位
| PR 里的路径 | 现在的路径 |
|---|---|
components/transcript/markdown.tsx |
features/transcript/ui/markdown.public.tsx(改名 + 搬家 + 加 .public);插入点从旧 :259 变成 base 的 :255 与 :270 之间 |
components/transcript/webui-mermaid.tsx |
features/transcript/ui/webui-mermaid.tsx |
styles/shell.css(.webui-mermaid 区块) |
features/transcript/ui/transcript.css:736-786(.webui-code-block 样式已经搬过去了) |
test/webui-browser/server.mjs |
packages/webui/test/browser/server.mjs |
test/webui-browser/transcript.spec.mjs |
packages/webui/test/browser/transcript.spec.mjs |
scripts/webui-dependency-rules.mjs |
scripts/lib/webui-dependency-rules.mjs |
三处硬门槛
base 的浏览器测试服务器不 Serve assets/。 packages/webui/test/browser/server.mjs:17-21 只 Serve index.html / client.js / styles.css。开了 splitting: true 之后 chunk 会 404,浏览器用例必红——这正是你改 test/webui-browser/server.mjs 的原因,而那个文件已经改名搬走了。
splitting: true 是全局副作用。 base 客户端已经有动态 import(features/composer/ui/WebuiComposer.tsx:153 的 await import("./slash-palette.js")),开了 splitting 它也会被切成 assets/ chunk,改动面不止 mermaid。composer 的斜杠面板要一起回归。
发布门会审 markdown.public.tsx。 好消息:import("mermaid") 是裸 specifier,不算 foreign,不会被 foreign-dynamic 拦,静态 import 同 business 私有兄弟也允许——这条路是通的。
自身的缺陷
主题完全没接。 base 有完整的明暗主题(styles/tokens.css:633-634 的 .dark 重绑语义 token、settings/projection/settings-view.ts:25 的 resolveThemePreference),而 initialize({startOnLoad:false, securityLevel:"strict"}) 没有传 theme,mermaid 用自带默认亮色主题,SVG 内联自己的配色——深色模式下图表会割裂。PR 自己的 CSS 注释也承认「The SVG itself carries Mermaid's own inline styles」。
不可信内容直接进渲染器。 dangerouslySetInnerHTML={{__html: state.svg}} 的内容是模型输出,可能被仓库内容或提示注入影响。katex 和 hljs 的输出由库自己转义,mermaid 是把用户文本渲染进 SVG,风险面不一样。securityLevel: "strict" 是 mermaid 自己的约束,不等于你这一层做了净化。
流式半截围栏会反复全量重渲染。 base 在流式期间直接对 part.text 跑 WebuiMarkdown(features/transcript/ui/AssistantBody.tsx:87, 324, 330, 338),而 WebuiMermaid 的 effect 依赖是 [text]。没有 debounce,也没有「围栏是否闭合」的判断——流式输出一张图 = 几十到上百次全量解析 + 生成 SVG。
失败回退不等价。 PR 说回退是「byte for byte 等于 Mermaid 之前的样子」,但之前 mmd 走通用 code 路径,多行会有行号 gutter(markdown.public.tsx:273 的 codeFenceGutter),而 failed 态没有。
尺寸处理不足。 .webui-mermaid svg { max-width:100%; height:auto } 加容器 overflow:hidden:宽图被压到 100% 之后文字不可读,而且没有横向滚动容器或最小宽度。
断言只覆盖 happy path
单测用 vi.mock("mermaid") 全 mock,从不跑真库;renderToStaticMarkup 不执行 effect,所以 WebuiMermaid 的 .catch() 失败分支没有任何单测覆盖(只直接测了 WebuiMermaidBlock 的 failed prop)。真库只在浏览器用例里跑了一次成功路径,失败回退在浏览器里也没测。
没覆盖的:深色主题、宽图、流式半截围栏、SVG 里含 javascript: 或 <script> 时的净化。
两处夹带
三个 commit 里第二个(composer store 适配器)与 Mermaid 无关,而 base 的 composer 架构已经大改,不该随 Mermaid 一起移植;第三个是空提交 ci: retrigger。lockfile 里还夹带了 yaml 2.9.0 → 2.9.1 的升级。
依赖和登记
packages/webui/package.json 加 "mermaid": "12.1.0"(base 的 engines.node ">=22.19 <23 || >=24.2 <27" 满足它 @mermaid-js/parser@2.0.1 的 node ≥22.12)。mermaid 不需要进 release/webui-npm/package.json 的 dependencies——客户端被 esbuild 打包了,这一点你判断得对;但 files 含 client/,splitting 之后 client/assets/ 会多出约 11MB、105 个 chunk,预览包体积会明显变大。
release/public-source.json 要登记新文件,test/vitest-suites.json 要加新单测。层级分类 scripts/lib/webui-dependency-rules.mjs:521-527 把 client/features/<business>/ui/ 判为 bindings / feature-ui 并自动匹配,比当年手工登记 provenance 省事。
|
按评审意见关掉重提,内容由 #224 承担:在新目录结构下重做(features/transcript/ui/markdown.public.tsx + webui-mermaid.tsx + transcript.css,浏览器基础设施改到 packages/webui/test/browser/),补齐三处硬门槛(测试服务器 Serve assets/、两个构建脚本开 splitting 并由 composer 斜杠面板的浏览器用例作 chunk 回归、冻结路径元数据 oracle 登记新文件),并修掉自身三处缺陷:主题改走注入的 DOM adapter 读应用主题(不再用 mermaid 自带浅色调色板)、SVG 进 DOM 前过滤 script/on*/javascript:/foreignObject 且关掉 htmlLabels、浏览器用例改跑真库并覆盖失败回退/主题/宽图/半截围栏。旧 PR 的两处夹带(composer store 适配器提交、空的 ci: retrigger 提交)与 lockfile 的 yaml 升级这次都没有,lockfile 是纯新增 853 行零删除。 |
Resubmission of #125 at the relocated paths, with the review's defects addressed. The acceptance table carried a "markdown-图表(mmd) 通过" row while the source had zero Mermaid support: a ```mermaid fence fell through to the plain code path, so a model-drawn flowchart arrived as an unreadable blob of text. The fence branch sits in markdown.public.tsx between the math and generic code branches and fires only for the mermaid/mmd language words; every other fence keeps its untouched path. Three defects from the review, fixed here: - Theme. Mermaid bakes its palette into the SVG it returns, so a diagram drawn with the light palette is a light diagram even on a dark page. The renderer now draws with the app's own theme, read through the injected DOM adapter (readDocumentTheme) — the dependency rules confine the ambient document to the platform layer, so the component must not touch it. The library is re-initialised when the theme changes, so the next diagram follows a switch. - Untrusted content. The diagram body is model output and the rendered SVG is injected with dangerouslySetInnerHTML, so the SVG is filtered before it reaches the DOM: scripts, event handlers, script-ish URLs and foreign documents are removed, and the drawing itself is left alone. Mermaid's own strict mode stays the primary control; the filter is the second layer. htmlLabels is off, so the diagram is pure SVG and the labels are escaped text rather than HTML in a foreignObject. - Tests. The unit suite mocks the library (Node has no DOM for it), so the browser cases run the real one: a diagram renders to SVG, an invalid diagram falls back to the plain fence, the diagram follows the app's theme, a wide diagram fits its container, and a half-streamed fence falls back instead of breaking the transcript. The unit suite covers the sanitizer and the theme read. Build: the client bundle is split, so a dynamic import is a chunk instead of inlined source. Measured on the built artifact: the entry goes 4,647,161 B to 4,456,586 B (shared code moves into chunks) and mermaid's ~12 MB lands in 109 lazy chunks under assets/. The browser test server now serves that prefix, matching the loopback service's static resolver; the composer's slash palette — already a dynamic import — is chunked too, and its browser case covers the regression.
Resubmission of #125 at the relocated paths, with the review's defects addressed. The acceptance table carried a "markdown-图表(mmd) 通过" row while the source had zero Mermaid support: a ```mermaid fence fell through to the plain code path, so a model-drawn flowchart arrived as an unreadable blob of text. The fence branch sits in markdown.public.tsx between the math and generic code branches and fires only for the mermaid/mmd language words; every other fence keeps its untouched path. Three defects from the review, fixed here: - Theme. Mermaid bakes its palette into the SVG it returns, so a diagram drawn with the light palette is a light diagram even on a dark page. The renderer now draws with the app's own theme, read through the injected DOM adapter (readDocumentTheme) — the dependency rules confine the ambient document to the platform layer, so the component must not touch it. The library is re-initialised when the theme changes, so the next diagram follows a switch. - Untrusted content. The diagram body is model output and the rendered SVG is injected with dangerouslySetInnerHTML, so the SVG is filtered before it reaches the DOM: scripts, event handlers, script-ish URLs and foreign documents are removed, and the drawing itself is left alone. Mermaid's own strict mode stays the primary control; the filter is the second layer. htmlLabels is off, so the diagram is pure SVG and the labels are escaped text rather than HTML in a foreignObject. - Tests. The unit suite mocks the library (Node has no DOM for it), so the browser cases run the real one: a diagram renders to SVG, an invalid diagram falls back to the plain fence, the diagram follows the app's theme, a wide diagram fits its container, and a half-streamed fence falls back instead of breaking the transcript. The unit suite covers the sanitizer and the theme read. Build: the client bundle is split, so a dynamic import is a chunk instead of inlined source. Measured on the built artifact: the entry goes 4,647,161 B to 4,456,586 B (shared code moves into chunks) and mermaid's ~12 MB lands in 109 lazy chunks under assets/. The browser test server now serves that prefix, matching the loopback service's static resolver; the composer's slash palette — already a dynamic import — is chunked too, and its browser case covers the regression.
feat(webui): transcript 渲染 Mermaid 围栏(#125 重提:新路径 + 主题 + 净化)
内容
关闭验收表「markdown-图表(mmd) 通过」一行——此前验收表与源码唯一对不上的地方:grep mermaid 零命中,模型生成的流程图/时序图在对话里是纯文本。
markdown.tsx在 math 分支后、通用 code 分支前新增 mermaid 分支(mermaid/mmd,大小写不敏感,支持title="a.mmd"属性写法),其余语言一行未碰webui-mermaid.tsx:动态 import 加载,语法错 / 空图 / 抛错 / 超时四种失败全部回退纯文本围栏(无 gutter)webui-mermaid样式,全部使用现有 design token<svg>,摘掉分支转红)产物大小(metafile 实测)
验证
ok: true(新文件按兄弟组件层登记bindings)