feat(frontend): mermaid diagrams + preview card auto-close fix - #434
feat(frontend): mermaid diagrams + preview card auto-close fix#434dimakis wants to merge 8 commits into
Conversation
dimakis
left a comment
There was a problem hiding this comment.
Centaur Review
Found 6 issue(s) (2 warning).
frontend/src/components/MermaidBlock.tsx
Solid PR with good test coverage for mermaid rendering and session state tracking. Main concerns: module-scoped mutable counter for mermaid IDs could cause issues under concurrent rendering, and the transport default flip lacks EventSource feature detection as a safety net.
- 🟡 unsafe_assumptions (L23): Module-scoped
let renderCounter = 0is shared across all MermaidBlock instances. In React StrictMode (dev) or concurrent rendering, effects run twice per mount, causing counter skips. More importantly, if two MermaidBlock instances render concurrently,++renderCounteris not atomic with the subsequentmermaid.render(id, code)— a second component can increment the counter between the first's increment and its render call, yielding duplicate or mismatched IDs. Consider usinguseId()(React 18+) oruseRefwith a per-instance counter.[fixable] - 🔵 style (L5):
mermaid.initialize()runs at module import time as a side effect. This hard-codes the dark theme — if the app ever supports light mode (or the user hasuseThemeset to light), diagrams will render with dark theme regardless. Consider deferring initialization or reading the current theme.[fixable]
frontend/src/client-store.ts
Solid PR with good test coverage for mermaid rendering and session state tracking. Main concerns: module-scoped mutable counter for mermaid IDs could cause issues under concurrent rendering, and the transport default flip lacks EventSource feature detection as a safety net.
- 🟡 regressions (L27): Default transport flipped from WS to SSE. Any user without
mitzo:transportin localStorage will silently switch transports on next load. The SSE path is well-tested, but there's no runtime feature detection — ifEventSourceis unavailable (e.g. some Capacitor WebView edge cases), no graceful fallback to WS occurs. Consider addingtypeof EventSource !== 'undefined'to the condition.[fixable]
frontend/src/lib/markdown-config.tsx
Solid PR with good test coverage for mermaid rendering and session state tracking. Main concerns: module-scoped mutable counter for mermaid IDs could cause issues under concurrent rendering, and the transport default flip lacks EventSource feature detection as a safety net.
- 🔵 missing_tests (L28): The new
precomponent inmarkdownComponents(used by FileViewer and MarkdownPreviewCard) has mermaid detection logic duplicated from MessageBubble, but no test coverage. If this logic diverges or breaks, there's no safety net. Consider adding a test formarkdownComponents.presimilar to the MessageBubble tests.[fixable] - 🔵 style (L15): The
classNameattribute allowlist for<code>elements (code: [...(defaultSchema.attributes?.code ?? []), 'className']) is necessary for mermaid detection but is quite broad — it allows any className through the sanitizer. Since onlylanguage-*classes are needed (set by rehype-highlight), consider restricting to a regex pattern if rehype-sanitize supports it, to avoid unintended class injection in the sanitized markdown context.[fixable]
packages/client/src/protocol-parser.ts
Solid PR with good test coverage for mermaid rendering and session state tracking. Main concerns: module-scoped mutable counter for mermaid IDs could cause issues under concurrent rendering, and the transport default flip lacks EventSource feature detection as a safety net.
- 🔵 missing_tests (L295): The
session_state_changedmessage type is handled in the protocol parser but is not defined in the frontend'sServerMessagetype union (frontend/src/types/ws-messages.ts). While the parser uses a genericmsgobject and works at runtime, adding the type to the union would provide compile-time safety and document the protocol contract on the client side.[fixable]
| }, | ||
| }); | ||
|
|
||
| let renderCounter = 0; |
There was a problem hiding this comment.
🟡 unsafe_assumptions: Module-scoped let renderCounter = 0 is shared across all MermaidBlock instances. In React StrictMode (dev) or concurrent rendering, effects run twice per mount, causing counter skips. More importantly, if two MermaidBlock instances render concurrently, ++renderCounter is not atomic with the subsequent mermaid.render(id, code) — a second component can increment the counter between the first's increment and its render call, yielding duplicate or mismatched IDs. Consider using useId() (React 18+) or useRef with a per-instance counter. [fixable]
| import mermaid from 'mermaid'; | ||
| import { CopyButton } from './CopyButton'; | ||
|
|
||
| mermaid.initialize({ |
There was a problem hiding this comment.
🔵 style: mermaid.initialize() runs at module import time as a side effect. This hard-codes the dark theme — if the app ever supports light mode (or the user has useTheme set to light), diagrams will render with dark theme regardless. Consider deferring initialization or reading the current theme. [fixable]
| * Revert SSE: localStorage.removeItem('mitzo:transport'); location.reload(); | ||
| */ | ||
| const useSSE = typeof window !== 'undefined' && localStorage.getItem('mitzo:transport') === 'sse'; | ||
| const useSSE = typeof window !== 'undefined' && localStorage.getItem('mitzo:transport') !== 'ws'; |
There was a problem hiding this comment.
🟡 regressions: Default transport flipped from WS to SSE. Any user without mitzo:transport in localStorage will silently switch transports on next load. The SSE path is well-tested, but there's no runtime feature detection — if EventSource is unavailable (e.g. some Capacitor WebView edge cases), no graceful fallback to WS occurs. Consider adding typeof EventSource !== 'undefined' to the condition. [fixable]
| <table {...props}>{children}</table> | ||
| </div> | ||
| ), | ||
| pre: ({ children, ...props }) => { |
There was a problem hiding this comment.
🔵 missing_tests: The new pre component in markdownComponents (used by FileViewer and MarkdownPreviewCard) has mermaid detection logic duplicated from MessageBubble, but no test coverage. If this logic diverges or breaks, there's no safety net. Consider adding a test for markdownComponents.pre similar to the MessageBubble tests. [fixable]
| attributes: { | ||
| ...defaultSchema.attributes, | ||
| img: [...(defaultSchema.attributes?.img ?? []), 'width', 'height'], | ||
| code: [...(defaultSchema.attributes?.code ?? []), 'className'], |
There was a problem hiding this comment.
🔵 style: The className attribute allowlist for <code> elements (code: [...(defaultSchema.attributes?.code ?? []), 'className']) is necessary for mermaid detection but is quite broad — it allows any className through the sanitizer. Since only language-* classes are needed (set by rehype-highlight), consider restricting to a regex pattern if rehype-sanitize supports it, to avoid unintended class injection in the sanitized markdown context. [fixable]
| callbacks.onSessionRenamed?.(msg.name as string); | ||
| break; | ||
|
|
||
| case 'session_state_changed': |
There was a problem hiding this comment.
🔵 missing_tests: The session_state_changed message type is handled in the protocol parser but is not defined in the frontend's ServerMessage type union (frontend/src/types/ws-messages.ts). While the parser uses a generic msg object and works at runtime, adding the type to the union would provide compile-time safety and document the protocol contract on the client side. [fixable]
…to-close Add mermaid.js support to render diagrams inline instead of showing raw code. Mermaid code blocks are detected in the pre component handler and rendered via a new MermaidBlock component with dark theme styling. Fix MarkdownPreviewCard auto-close bug by memoizing the ReactMarkdown components object in TextBubble. Previously, every parent re-render recreated the components with new function references, causing react-markdown to unmount and remount the tree, resetting expanded state. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Replace module-scoped renderCounter with React useId() for stable, concurrent-safe mermaid element IDs - Defer mermaid.initialize() to first render instead of module import - Restrict rehype-sanitize className allowlist to language-* pattern - Add markdown-config pre component test coverage Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
00d1cc3 to
01e46b8
Compare
dimakis
left a comment
There was a problem hiding this comment.
Centaur Review
Found 7 issue(s) (2 warning).
frontend/src/components/MermaidBlock.tsx
Solid feature addition with good test coverage of the routing logic and a sensible useMemo fix for component stability. Main concerns: explicitly set mermaid's securityLevel: 'strict', verify the markdown-config pre handler didn't regress copy-button behavior in FileViewer/preview cards, and consider deduplicating the mermaid detection logic.
- 🟡 unsafe_assumptions (L10): Mermaid's
securityLevelis not explicitly set. While mermaid v11 defaults to'strict'(which uses DOMPurify), relying on a library default for security is fragile — a future upgrade could change the default. Explicitly setsecurityLevel: 'strict'in themermaid.initialize()call to make the security posture intentional and auditable.[fixable] - 🔵 style (L35):
containerRefis assigned to the div'srefprop but never read anywhere in the component. It's dead code — remove it unless there's a planned use (e.g., zoom/pan).[fixable] - 🔵 missing_tests: There are no unit tests for the
MermaidBlockcomponent itself — only for the detection/routing logic in theprehandler. The error fallback path (rendering a plain code block), the loading state (svgis null), and the cleanup of leftover DOM elements on render failure are untested. A component-level test (mockingmermaid.render) would cover these branches. - 🔵 bugs (L66):
instanceId(fromuseId()) is used inside the effect but omitted from the dependency array[code]. This is technically safe becauseuseId()returns a stable value, but it will trigger anreact-hooks/exhaustive-depslint warning if that rule is enabled. Adding it to the array is a no-op at runtime and silences the lint.[fixable] - 🔵 unsafe_assumptions (L5): The module-level
mermaidInitializedflag works in production but can leak state between test runs in Vitest (which reuses the module cache by default). If MermaidBlock tests are added later, they may see stale initialization. Not a production issue, but worth noting for testability.
frontend/src/lib/markdown-config.tsx
Solid feature addition with good test coverage of the routing logic and a sensible useMemo fix for component stability. Main concerns: explicitly set mermaid's securityLevel: 'strict', verify the markdown-config pre handler didn't regress copy-button behavior in FileViewer/preview cards, and consider deduplicating the mermaid detection logic.
- 🟡 regressions (L29): The
prehandler inmarkdown-config.tsx(used by FileViewer and MarkdownPreviewCard) renders a bare<pre>for non-mermaid code blocks, while theprehandler inMessageBubble.tsxwraps them in acode-block-wrapperdiv with aCopyButton. This means code blocks rendered viamarkdown-config(file viewer, preview cards) lost their copy button and wrapper styling in this PR, since theprehandler was changed from a passthrough to one that actively returns<pre>without the wrapper. Verify this is intentional — if those contexts previously had copy buttons, this is a regression.[fixable]
frontend/src/components/MessageBubble.tsx
Solid feature addition with good test coverage of the routing logic and a sensible useMemo fix for component stability. Main concerns: explicitly set mermaid's securityLevel: 'strict', verify the markdown-config pre handler didn't regress copy-button behavior in FileViewer/preview cards, and consider deduplicating the mermaid detection logic.
- 🔵 style (L114): The mermaid detection logic (extracting the first child, checking className against
/language-mermaid/) is duplicated verbatim betweenMessageBubble.tsx:114-123andmarkdown-config.tsx:29-37. Extract a shared helper (e.g.,isMermaidCodeBlock(children): string | null) to keep the two call sites in sync.[fixable]
| function ensureMermaidInit() { | ||
| if (mermaidInitialized) return; | ||
| mermaidInitialized = true; | ||
| mermaid.initialize({ |
There was a problem hiding this comment.
🟡 unsafe_assumptions: Mermaid's securityLevel is not explicitly set. While mermaid v11 defaults to 'strict' (which uses DOMPurify), relying on a library default for security is fragile — a future upgrade could change the default. Explicitly set securityLevel: 'strict' in the mermaid.initialize() call to make the security posture intentional and auditable. [fixable]
|
|
||
| export function MermaidBlock({ code }: MermaidBlockProps) { | ||
| const instanceId = useId(); | ||
| const containerRef = useRef<HTMLDivElement>(null); |
There was a problem hiding this comment.
🔵 style: containerRef is assigned to the div's ref prop but never read anywhere in the component. It's dead code — remove it unless there's a planned use (e.g., zoom/pan). [fixable]
| return () => { | ||
| cancelled = true; | ||
| }; | ||
| }, [code]); |
There was a problem hiding this comment.
🔵 bugs: instanceId (from useId()) is used inside the effect but omitted from the dependency array [code]. This is technically safe because useId() returns a stable value, but it will trigger an react-hooks/exhaustive-deps lint warning if that rule is enabled. Adding it to the array is a no-op at runtime and silences the lint. [fixable]
| import mermaid from 'mermaid'; | ||
| import { CopyButton } from './CopyButton'; | ||
|
|
||
| let mermaidInitialized = false; |
There was a problem hiding this comment.
🔵 unsafe_assumptions: The module-level mermaidInitialized flag works in production but can leak state between test runs in Vitest (which reuses the module cache by default). If MermaidBlock tests are added later, they may see stale initialization. Not a production issue, but worth noting for testability.
| <table {...props}>{children}</table> | ||
| </div> | ||
| ), | ||
| pre: ({ children, ...props }) => { |
There was a problem hiding this comment.
🟡 regressions: The pre handler in markdown-config.tsx (used by FileViewer and MarkdownPreviewCard) renders a bare <pre> for non-mermaid code blocks, while the pre handler in MessageBubble.tsx wraps them in a code-block-wrapper div with a CopyButton. This means code blocks rendered via markdown-config (file viewer, preview cards) lost their copy button and wrapper styling in this PR, since the pre handler was changed from a passthrough to one that actively returns <pre> without the wrapper. Verify this is intentional — if those contexts previously had copy buttons, this is a regression. [fixable]
| <table {...props}>{children}</table> | ||
| </div> | ||
| ), | ||
| pre: ({ children, ...props }: React.ComponentProps<'pre'>) => { |
There was a problem hiding this comment.
🔵 style: The mermaid detection logic (extracting the first child, checking className against /language-mermaid/) is duplicated verbatim between MessageBubble.tsx:114-123 and markdown-config.tsx:29-37. Extract a shared helper (e.g., isMermaidCodeBlock(children): string | null) to keep the two call sites in sync. [fixable]
- Add securityLevel: 'strict' to mermaid.initialize() - Remove dead containerRef - Add instanceId to useEffect dependency array - Extract shared getMermaidCode() helper to deduplicate detection logic between MessageBubble and markdown-config - Add MermaidBlock component tests Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
dimakis
left a comment
There was a problem hiding this comment.
Centaur Review
Found 5 issue(s) (2 warning).
frontend/src/components/__tests__/MermaidBlock.test.ts
Solid mermaid integration with correct security posture (strict mode + sanitize schema). Main gaps are weak test coverage for MermaidBlock's async render/error paths and an unanchored detection regex.
- 🟡 missing_tests (L35): The test titled 'renders fallback code block on render error' is misleading — it only verifies the initial render (empty string) before the async error resolves, not the actual error fallback UI. renderToStaticMarkup doesn't run useEffect, so none of the three tests exercise the successful SVG render path or the error fallback code-block path. The core behavior of this component (rendering SVG output and falling back on errors) is entirely untested. Consider using @testing-library/react with act() to test async state updates.
[fixable]
frontend/src/lib/mermaid-detect.ts
Solid mermaid integration with correct security posture (strict mode + sanitize schema). Main gaps are weak test coverage for MermaidBlock's async render/error paths and an unanchored detection regex.
- 🟡 unsafe_assumptions (L12): The regex /language-mermaid/ is unanchored and would match 'language-mermaid-extended' or 'hljs language-mermaid' (multi-class className strings). Use /\blanguage-mermaid\b/ or an exact check like className.split(/\s+/).includes('language-mermaid') for precise matching.
[fixable]
frontend/src/components/MessageBubble.tsx
Solid mermaid integration with correct security posture (strict mode + sanitize schema). Main gaps are weak test coverage for MermaidBlock's async render/error paths and an unanchored detection regex.
- 🔵 style (L115): The mermaid-aware
precomponent is duplicated between MessageBubble.tsx (line 115-124) and markdown-config.tsx (line 28-32). Both call getMermaidCode and return MermaidBlock identically; only the non-mermaid fallback differs (CopyButton wrapper vs. plain). Consider extracting the shared mermaid detection + fallback pattern, or having MessageBubble compose on top of markdown-config's pre.
[fixable]
frontend/src/components/MermaidBlock.tsx
Solid mermaid integration with correct security posture (strict mode + sanitize schema). Main gaps are weak test coverage for MermaidBlock's async render/error paths and an unanchored detection regex.
- 🔵 bugs (L56): The DOM cleanup
document.getElementById(d${id})?.remove()runs even whencancelledis true (it's outside theif (!cancelled)block). If the component unmounts during a failed render, this removes a DOM element that may have already been cleaned up or belong to a new render cycle. Move it inside theif (!cancelled)guard for consistency, or document why it must always run.[fixable] - 🔵 unsafe_assumptions (L5): The module-level
mermaidInitializedflag means mermaid.initialize() is only ever called once with the hardcoded dark theme. If theme support is added later (light/dark toggle), re-initialization would be silently skipped. This is fine for now but worth noting as a design constraint.
| expect(html).toBe(''); | ||
| }); | ||
|
|
||
| it('renders fallback code block on render error', async () => { |
There was a problem hiding this comment.
🟡 missing_tests: The test titled 'renders fallback code block on render error' is misleading — it only verifies the initial render (empty string) before the async error resolves, not the actual error fallback UI. renderToStaticMarkup doesn't run useEffect, so none of the three tests exercise the successful SVG render path or the error fallback code-block path. The core behavior of this component (rendering SVG output and falling back on errors) is entirely untested. Consider using @testing-library/react with act() to test async state updates. [fixable]
| const child = React.Children.toArray(children)[0]; | ||
| if (!React.isValidElement(child)) return null; | ||
| const className = (child.props as Record<string, unknown>)?.className; | ||
| if (typeof className === 'string' && /language-mermaid/.test(className)) { |
There was a problem hiding this comment.
🟡 unsafe_assumptions: The regex /language-mermaid/ is unanchored and would match 'language-mermaid-extended' or 'hljs language-mermaid' (multi-class className strings). Use /\blanguage-mermaid\b/ or an exact check like className.split(/\s+/).includes('language-mermaid') for precise matching. [fixable]
| <table {...props}>{children}</table> | ||
| </div> | ||
| ), | ||
| pre: ({ children, ...props }: React.ComponentProps<'pre'>) => { |
There was a problem hiding this comment.
🔵 style: The mermaid-aware pre component is duplicated between MessageBubble.tsx (line 115-124) and markdown-config.tsx (line 28-32). Both call getMermaidCode and return MermaidBlock identically; only the non-mermaid fallback differs (CopyButton wrapper vs. plain
). Consider extracting the shared mermaid detection + fallback pattern, or having MessageBubble compose on top of markdown-config's pre. [fixable]
| setError('Invalid diagram'); | ||
| setSvg(null); | ||
| } | ||
| document.getElementById(`d${id}`)?.remove(); |
There was a problem hiding this comment.
🔵 bugs: The DOM cleanup document.getElementById(d${id})?.remove() runs even when cancelled is true (it's outside the if (!cancelled) block). If the component unmounts during a failed render, this removes a DOM element that may have already been cleaned up or belong to a new render cycle. Move it inside the if (!cancelled) guard for consistency, or document why it must always run. [fixable]
| import mermaid from 'mermaid'; | ||
| import { CopyButton } from './CopyButton'; | ||
|
|
||
| let mermaidInitialized = false; |
There was a problem hiding this comment.
🔵 unsafe_assumptions: The module-level mermaidInitialized flag means mermaid.initialize() is only ever called once with the hardcoded dark theme. If theme support is added later (light/dark toggle), re-initialization would be silently skipped. This is fine for now but worth noting as a design constraint.
dimakis
left a comment
There was a problem hiding this comment.
Centaur Review
Found 6 issue(s) (3 warning).
frontend/src/components/__tests__/MermaidBlock.test.ts
Solid feature addition with correct security posture (strict mermaid + sanitized classNames) and a good useMemo fix for the preview card auto-close bug; main gaps are in test coverage for the async render path and a substring-matching regex in mermaid detection.
- 🟡 missing_tests: All three tests use
renderToStaticMarkupwhich skipsuseEffect— so the success path (SVG rendered and injected via dangerouslySetInnerHTML) is never tested. The error-fallback test also only verifies the initial render (''), not the error state. Consider using@testing-library/react'srender+waitForto test the async render and error states.[fixable]
frontend/src/lib/mermaid-detect.ts
Solid feature addition with correct security posture (strict mermaid + sanitized classNames) and a good useMemo fix for the preview card auto-close bug; main gaps are in test coverage for the async render path and a substring-matching regex in mermaid detection.
- 🔵 missing_tests: No standalone unit tests for
getMermaidCode. It's indirectly tested via markdown-config.test.ts and MessageBubble.test.ts, but edge cases (multiple children, non-element children, className as an array) are untested.[fixable] - 🟡 unsafe_assumptions (L12): The regex
/language-mermaid/matches as a substring — e.g.language-mermaid-customwould also match. Use/^language-mermaid$/(or=== 'language-mermaid') for an exact match, consistent with how rehype-highlight sets class names.[fixable]
frontend/src/lib/markdown-config.tsx
Solid feature addition with correct security posture (strict mermaid + sanitized classNames) and a good useMemo fix for the preview card auto-close bug; main gaps are in test coverage for the async render path and a substring-matching regex in mermaid detection.
- 🔵 regressions (L28): The
prehandler inmarkdownComponents(used by FileViewer and MarkdownPreviewCard) does not wrap non-mermaid code blocks incode-block-wrapperwith a CopyButton, unlike the identical handler in TextBubble. This creates an inconsistency: code blocks in chat have copy buttons, but code blocks in the file viewer and preview cards do not.[fixable]
frontend/src/components/MermaidBlock.tsx
Solid feature addition with correct security posture (strict mermaid + sanitized classNames) and a good useMemo fix for the preview card auto-close bug; main gaps are in test coverage for the async render path and a substring-matching regex in mermaid detection.
- 🟡 bugs (L56): The cleanup
document.getElementById(d${id})?.remove()is only called inside thecatchblock, but mermaid.render() also creates a temporary DOM element on success. If the component unmounts whilemermaid.render()is in-flight (cancelled = true), the temporary element created by mermaid may leak in the DOM. Consider also cleaning up the element in the effect's cleanup function.[fixable] - 🔵 style (L5): Module-level
let mermaidInitializedis mutable shared state. If tests reset modules or if HMR reloads the component without reloading the module, the flag could get out of sync. Consider usingmermaid.initializeidempotency or checkingmermaidinternal state instead.
| const child = React.Children.toArray(children)[0]; | ||
| if (!React.isValidElement(child)) return null; | ||
| const className = (child.props as Record<string, unknown>)?.className; | ||
| if (typeof className === 'string' && /language-mermaid/.test(className)) { |
There was a problem hiding this comment.
🟡 unsafe_assumptions: The regex /language-mermaid/ matches as a substring — e.g. language-mermaid-custom would also match. Use /^language-mermaid$/ (or === 'language-mermaid') for an exact match, consistent with how rehype-highlight sets class names. [fixable]
| <table {...props}>{children}</table> | ||
| </div> | ||
| ), | ||
| pre: ({ children, ...props }) => { |
There was a problem hiding this comment.
🔵 regressions: The pre handler in markdownComponents (used by FileViewer and MarkdownPreviewCard) does not wrap non-mermaid code blocks in code-block-wrapper with a CopyButton, unlike the identical handler in TextBubble. This creates an inconsistency: code blocks in chat have copy buttons, but code blocks in the file viewer and preview cards do not. [fixable]
| setError('Invalid diagram'); | ||
| setSvg(null); | ||
| } | ||
| document.getElementById(`d${id}`)?.remove(); |
There was a problem hiding this comment.
🟡 bugs: The cleanup document.getElementById(d${id})?.remove() is only called inside the catch block, but mermaid.render() also creates a temporary DOM element on success. If the component unmounts while mermaid.render() is in-flight (cancelled = true), the temporary element created by mermaid may leak in the DOM. Consider also cleaning up the element in the effect's cleanup function. [fixable]
| import mermaid from 'mermaid'; | ||
| import { CopyButton } from './CopyButton'; | ||
|
|
||
| let mermaidInitialized = false; |
There was a problem hiding this comment.
🔵 style: Module-level let mermaidInitialized is mutable shared state. If tests reset modules or if HMR reloads the component without reloading the module, the flag could get out of sync. Consider using mermaid.initialize idempotency or checking mermaid internal state instead.
…tests - Anchor mermaid detection regex with \b word boundaries - Move DOM cleanup inside !cancelled guard - Improve MermaidBlock tests: jsdom environment, async render/error paths, securityLevel verification Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
dimakis
left a comment
There was a problem hiding this comment.
Centaur Review
Found 6 issue(s) (1 warning).
frontend/src/components/MessageBubble.tsx
Well-structured mermaid rendering feature with good security defaults (strict mode, sanitized classes) and solid test coverage, but the useMemo dependency on navigate may undermine the preview card auto-close fix — verify navigate stability or use a ref pattern.
- 🟡 unsafe_assumptions (L169): useMemo deps include
navigatefrom useNavigate(). In React Router v7 declarative mode (BrowserRouter), navigate's reference stability is not guaranteed across renders — it depends on internal useCallback deps including context values. If navigate changes identity on re-renders that don't involve navigation (e.g. during streaming updates), the memoization is defeated and MarkdownPreviewCard expanded state will still reset — the exact bug this PR aims to fix. Consider using a ref for navigate (const navRef = useRef(navigate); navRef.current = navigate;) and referencingnavRef.currentinside the memo, removingnavigatefrom the dep array.[fixable]
frontend/src/lib/markdown-config.tsx
Well-structured mermaid rendering feature with good security defaults (strict mode, sanitized classes) and solid test coverage, but the useMemo dependency on navigate may undermine the preview card auto-close fix — verify navigate stability or use a ref pattern.
- 🔵 regressions (L29): The
prehandler in markdown-config.tsx (used by MarkdownPreviewCard/FileViewer) does not include a CopyButton in its non-mermaid fallback, while MessageBubble'sprehandler does. This means code blocks in file previews lose their copy button. If this was intentional, consider adding a brief comment; if not, add CopyButton here too for consistency.[fixable] - 🔵 style (L14): Comment says 'Only allow language-* classes (set by rehype-highlight)' but rehype-highlight is not used in this rendering path (MarkdownPreviewCard/FileViewer use rehypeRaw + rehypeSanitize, not rehypeHighlight). The classes come from the markdown parser's fenced code block syntax. Misleading comment could confuse future readers.
[fixable]
frontend/src/lib/mermaid-detect.ts
Well-structured mermaid rendering feature with good security defaults (strict mode, sanitized classes) and solid test coverage, but the useMemo dependency on navigate may undermine the preview card auto-close fix — verify navigate stability or use a ref pattern.
- 🔵 missing_tests: getMermaidCode() has no dedicated unit test file. It is indirectly tested via MessageBubble and markdown-config tests, but a direct test would cover edge cases: children with multiple elements, non-element children, className as an array, className containing multiple space-separated classes like 'hljs language-mermaid' (as rehype-highlight produces).
[fixable]
frontend/src/components/__tests__/MermaidBlock.test.ts
Well-structured mermaid rendering feature with good security defaults (strict mode, sanitized classes) and solid test coverage, but the useMemo dependency on navigate may undermine the preview card auto-close fix — verify navigate stability or use a ref pattern.
- 🔵 missing_tests: No test for the unmount-during-render scenario: start rendering, unmount the component before mermaid.render resolves, verify no state updates or DOM manipulations occur. This was the subject of a previous review fix (DOM cleanup moved inside !cancelled guard) and deserves regression coverage.
[fixable]
frontend/src/components/MermaidBlock.tsx
Well-structured mermaid rendering feature with good security defaults (strict mode, sanitized classes) and solid test coverage, but the useMemo dependency on navigate may undermine the preview card auto-close fix — verify navigate stability or use a ref pattern.
- 🔵 unsafe_assumptions (L5): Module-level
mermaidInitializedflag won't reset during Vite HMR — if mermaid config (theme, security) needs updating during development, a full page reload is required. Minor DX issue; not a production concern.
| ); | ||
| }, | ||
| }), | ||
| [navigate, currentPath], |
There was a problem hiding this comment.
🟡 unsafe_assumptions: useMemo deps include navigate from useNavigate(). In React Router v7 declarative mode (BrowserRouter), navigate's reference stability is not guaranteed across renders — it depends on internal useCallback deps including context values. If navigate changes identity on re-renders that don't involve navigation (e.g. during streaming updates), the memoization is defeated and MarkdownPreviewCard expanded state will still reset — the exact bug this PR aims to fix. Consider using a ref for navigate (const navRef = useRef(navigate); navRef.current = navigate;) and referencing navRef.current inside the memo, removing navigate from the dep array. [fixable]
| </div> | ||
| ), | ||
| pre: ({ children, ...props }) => { | ||
| const mermaidCode = getMermaidCode(children); |
There was a problem hiding this comment.
🔵 regressions: The pre handler in markdown-config.tsx (used by MarkdownPreviewCard/FileViewer) does not include a CopyButton in its non-mermaid fallback, while MessageBubble's pre handler does. This means code blocks in file previews lose their copy button. If this was intentional, consider adding a brief comment; if not, add CopyButton here too for consistency. [fixable]
| attributes: { | ||
| ...defaultSchema.attributes, | ||
| img: [...(defaultSchema.attributes?.img ?? []), 'width', 'height'], | ||
| // Only allow language-* classes (set by rehype-highlight) — not arbitrary classNames |
There was a problem hiding this comment.
🔵 style: Comment says 'Only allow language-* classes (set by rehype-highlight)' but rehype-highlight is not used in this rendering path (MarkdownPreviewCard/FileViewer use rehypeRaw + rehypeSanitize, not rehypeHighlight). The classes come from the markdown parser's fenced code block syntax. Misleading comment could confuse future readers. [fixable]
| import mermaid from 'mermaid'; | ||
| import { CopyButton } from './CopyButton'; | ||
|
|
||
| let mermaidInitialized = false; |
There was a problem hiding this comment.
🔵 unsafe_assumptions: Module-level mermaidInitialized flag won't reset during Vite HMR — if mermaid config (theme, security) needs updating during development, a full page reload is required. Minor DX issue; not a production concern.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
dimakis
left a comment
There was a problem hiding this comment.
Centaur Review
Found 4 issue(s) (1 warning).
frontend/src/styles/global.css
Solid mermaid integration with good security posture (strict mode, sanitized classes). One functional bug: the CopyButton on rendered diagrams is invisible due to a missing CSS hover rule for .mermaid-block.
- 🟡 bugs (L2126): The CopyButton on rendered mermaid diagrams will be permanently invisible on desktop. The hover rule
.code-block-wrapper:hover .code-block-copy { opacity: 1 }targets.code-block-wrapper, but the mermaid success path uses.mermaid-blockas the parent. A matching rule.mermaid-block:hover .code-block-copy { opacity: 1 }is needed.[fixable]
frontend/src/lib/mermaid-detect.ts
Solid mermaid integration with good security posture (strict mode, sanitized classes). One functional bug: the CopyButton on rendered diagrams is invisible due to a missing CSS hover rule for .mermaid-block.
- 🔵 missing_tests: No dedicated unit test file for
mermaid-detect.ts. It's tested indirectly throughmarkdown-config.test.tsandMessageBubble.test.ts, but given the project's TDD requirement, amermaid-detect.test.tscovering edge cases (no children, non-element children, multiple children) would be appropriate.[fixable] - 🔵 style (L7): The JSDoc comment ('If children represent a mermaid code block...') duplicates what the function name and return type already express. Per the project's code style preference for no comments unless the WHY is non-obvious, this could be removed.
[fixable]
frontend/src/components/MermaidBlock.tsx
Solid mermaid integration with good security posture (strict mode, sanitized classes). One functional bug: the CopyButton on rendered diagrams is invisible due to a missing CSS hover rule for .mermaid-block.
- 🔵 unsafe_assumptions (L5): The module-level
mermaidInitializedflag never resets. In tests this creates ordering dependence — test 3 (renders SVG...) sets it totrue, so any later test expectingmermaid.initializeto be called again will fail silently. Consider exporting aresetMermaidInit()for test use, or usingvi.resetModules()in the test'sbeforeEach.[fixable]
| } | ||
| } | ||
|
|
||
| /* ===== Mermaid Diagrams ===== */ |
There was a problem hiding this comment.
🟡 bugs: The CopyButton on rendered mermaid diagrams will be permanently invisible on desktop. The hover rule .code-block-wrapper:hover .code-block-copy { opacity: 1 } targets .code-block-wrapper, but the mermaid success path uses .mermaid-block as the parent. A matching rule .mermaid-block:hover .code-block-copy { opacity: 1 } is needed. [fixable]
| /** | ||
| * If children represent a mermaid code block (language-mermaid class on the | ||
| * code element), returns the raw mermaid source. Otherwise returns null. | ||
| */ |
There was a problem hiding this comment.
🔵 style: The JSDoc comment ('If children represent a mermaid code block...') duplicates what the function name and return type already express. Per the project's code style preference for no comments unless the WHY is non-obvious, this could be removed. [fixable]
| import mermaid from 'mermaid'; | ||
| import { CopyButton } from './CopyButton'; | ||
|
|
||
| let mermaidInitialized = false; |
There was a problem hiding this comment.
🔵 unsafe_assumptions: The module-level mermaidInitialized flag never resets. In tests this creates ordering dependence — test 3 (renders SVG...) sets it to true, so any later test expecting mermaid.initialize to be called again will fail silently. Consider exporting a resetMermaidInit() for test use, or using vi.resetModules() in the test's beforeEach. [fixable]
dimakis
left a comment
There was a problem hiding this comment.
Centaur Review
Found 4 issue(s) (1 warning).
frontend/src/components/MermaidBlock.tsx
Well-structured mermaid rendering feature with good security posture (strict mode, sanitize schema) and correct useMemo fix for preview card state. Main concern is the static import of mermaid pulling ~1MB+ of dependencies (d3, cytoscape, katex) into the initial bundle of a mobile-first app — a dynamic import would preserve the UX while avoiding the bundle cost.
- 🟡 style (L2): Static
import mermaid from 'mermaid'pulls mermaid (plus d3, cytoscape, katex, roughjs, dompurify, etc.) into the main bundle. Since MermaidBlock is imported by MessageBubble.tsx (a core chat component), all these dependencies load on initial page load. For a mobile-first app this is a significant hit. Consider dynamically importing mermaid inside the useEffect (const { default: mermaid } = await import('mermaid')) so Vite can code-split it into a separate chunk loaded only when a mermaid diagram is actually encountered.[fixable] - 🔵 unsafe_assumptions (L5): The module-level
mermaidInitializedflag is never reset between tests. The MermaidBlock tests work becauserenderToStaticMarkup(tests 1-2) doesn't trigger useEffect, so test 3 is the first to callensureMermaidInit(). If test ordering changes or new tests are added that userender()before test 3, themermaid.initializeassertion in test 3 would fail. Consider exporting aresetMermaidInitfor test use, or moving the flag into a ref/context.[fixable]
frontend/src/lib/mermaid-detect.ts
Well-structured mermaid rendering feature with good security posture (strict mode, sanitize schema) and correct useMemo fix for preview card state. Main concern is the static import of mermaid pulling ~1MB+ of dependencies (d3, cytoscape, katex) into the initial bundle of a mobile-first app — a dynamic import would preserve the UX while avoiding the bundle cost.
- 🔵 missing_tests: No dedicated unit test file for
getMermaidCode. It's tested indirectly through MessageBubble and markdown-config tests, but edge cases likeclassNamebeing an array (possible in some rehype configurations), children beingundefined/null, or multiple code children are not covered. Afrontend/src/lib/__tests__/mermaid-detect.test.tswould improve coverage.[fixable]
frontend/src/components/MessageBubble.tsx
Well-structured mermaid rendering feature with good security posture (strict mode, sanitize schema) and correct useMemo fix for preview card state. Main concern is the static import of mermaid pulling ~1MB+ of dependencies (d3, cytoscape, katex) into the initial bundle of a mobile-first app — a dynamic import would preserve the UX while avoiding the bundle cost.
- 🔵 regressions (L169): The useMemo deps include
currentPath, so when the URL changes (e.g. navigating to FileViewer and back), the entire components object is recreated, which would reset MarkdownPreviewCard expanded state. This is the same behavior as before the fix (pre-useMemo), so it's not a regression per se, but it limits the fix: the auto-close bug is only prevented during same-page re-renders (streaming updates), not across navigation. Worth noting in case this was intended to be fully fixed.
| @@ -0,0 +1,86 @@ | |||
| import { useEffect, useId, useState } from 'react'; | |||
| import mermaid from 'mermaid'; | |||
There was a problem hiding this comment.
🟡 style: Static import mermaid from 'mermaid' pulls mermaid (plus d3, cytoscape, katex, roughjs, dompurify, etc.) into the main bundle. Since MermaidBlock is imported by MessageBubble.tsx (a core chat component), all these dependencies load on initial page load. For a mobile-first app this is a significant hit. Consider dynamically importing mermaid inside the useEffect (const { default: mermaid } = await import('mermaid')) so Vite can code-split it into a separate chunk loaded only when a mermaid diagram is actually encountered. [fixable]
| import mermaid from 'mermaid'; | ||
| import { CopyButton } from './CopyButton'; | ||
|
|
||
| let mermaidInitialized = false; |
There was a problem hiding this comment.
🔵 unsafe_assumptions: The module-level mermaidInitialized flag is never reset between tests. The MermaidBlock tests work because renderToStaticMarkup (tests 1-2) doesn't trigger useEffect, so test 3 is the first to call ensureMermaidInit(). If test ordering changes or new tests are added that use render() before test 3, the mermaid.initialize assertion in test 3 would fail. Consider exporting a resetMermaidInit for test use, or moving the flag into a ref/context. [fixable]
| ); | ||
| }, | ||
| }), | ||
| [navigate, currentPath], |
There was a problem hiding this comment.
🔵 regressions: The useMemo deps include currentPath, so when the URL changes (e.g. navigating to FileViewer and back), the entire components object is recreated, which would reset MarkdownPreviewCard expanded state. This is the same behavior as before the fix (pre-useMemo), so it's not a regression per se, but it limits the fix: the auto-close bug is only prevented during same-page re-renders (streaming updates), not across navigation. Worth noting in case this was intended to be fully fixed.
… cases - Convert mermaid to dynamic import() for code splitting (~1MB+ kept out of main bundle, loaded only when a diagram is encountered) - Export _resetMermaidInit for test cleanup between runs - Use exact class match (split+includes) instead of regex for mermaid detection — prevents false positives like language-mermaid-extended - Add comprehensive getMermaidCode edge case tests Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
dimakis
left a comment
There was a problem hiding this comment.
Centaur Review
Found 6 issue(s) (2 warning).
frontend/src/components/MermaidBlock.tsx
Well-structured feature addition with good test coverage and proper security settings; the main concern is the initialization flag being set before mermaid.initialize() executes, which could leave future instances in a broken state if init fails.
- 🟡 bugs (L30):
mermaidInitializedis set totruebeforemermaid.initialize()executes (line 30 runs before line 31). Ifinitialize()throws synchronously, the flag staystrueand all future MermaidBlock instances skip initialization —mermaid.render()then runs against an uninitialized library. Move the flag assignment after theinitialize()call so a failed init can be retried.[fixable] - 🟡 unsafe_assumptions (L85):
dangerouslySetInnerHTML={{ __html: svg }}relies entirely on mermaid'ssecurityLevel: 'strict'(which uses DOMPurify internally) for XSS safety. This is the standard mermaid integration pattern and is safe as long as the mermaid library itself isn't compromised. However, if the mermaid dependency were supply-chain attacked, SVG would be injected unsanitized. Consider an explicit DOMPurify pass on the SVG output as defense-in-depth, or at minimum a comment documenting the safety invariant.[fixable] - 🔵 style (L81): When
svgis null and there's no error (initial render / loading state), the component returnsnull— a blank gap in the message. Consider rendering a lightweight loading indicator (e.g., a skeleton or the raw code block) so the user sees something while mermaid loads (~1MB async import + render).[fixable]
frontend/src/components/__tests__/MermaidBlock.test.ts
Well-structured feature addition with good test coverage and proper security settings; the main concern is the initialization flag being set before mermaid.initialize() executes, which could leave future instances in a broken state if init fails.
- 🔵 missing_tests: No test covers the
cancelled = truecleanup path — e.g., unmounting the component before the asyncmermaid.render()resolves. Anact+cleanupbefore the mock resolves would verify thatsetSvg/setErrorare not called after unmount.[fixable] - 🔵 missing_tests: No test covers re-rendering when the
codeprop changes (the effect re-runs becausecodeis in the dependency array). Verifying thatmermaid.renderis called again with the new code — and that the old SVG is replaced — would strengthen confidence in the reactivity.[fixable]
frontend/src/lib/markdown-config.tsx
Well-structured feature addition with good test coverage and proper security settings; the main concern is the initialization flag being set before mermaid.initialize() executes, which could leave future instances in a broken state if init fails.
- 🔵 style (L28): The
prehandler here renders a plain<pre>for non-mermaid code blocks, while theprehandler inMessageBubble.tsxwraps them with aCopyButton. This is intentional (markdown-config serves MarkdownPreviewCard and FileViewer where CopyButton isn't needed), but the behavioral difference between two nearly identicalprehandlers in the same codebase could surprise a future contributor.
| // the main bundle — only loaded when a mermaid diagram is encountered. | ||
| const { default: mermaid } = await import('mermaid'); | ||
| if (!mermaidInitialized) { | ||
| mermaidInitialized = true; |
There was a problem hiding this comment.
🟡 bugs: mermaidInitialized is set to true before mermaid.initialize() executes (line 30 runs before line 31). If initialize() throws synchronously, the flag stays true and all future MermaidBlock instances skip initialization — mermaid.render() then runs against an uninitialized library. Move the flag assignment after the initialize() call so a failed init can be retried. [fixable]
|
|
||
| return ( | ||
| <div className="mermaid-block"> | ||
| <div className="mermaid-block-svg" dangerouslySetInnerHTML={{ __html: svg }} /> |
There was a problem hiding this comment.
🟡 unsafe_assumptions: dangerouslySetInnerHTML={{ __html: svg }} relies entirely on mermaid's securityLevel: 'strict' (which uses DOMPurify internally) for XSS safety. This is the standard mermaid integration pattern and is safe as long as the mermaid library itself isn't compromised. However, if the mermaid dependency were supply-chain attacked, SVG would be injected unsanitized. Consider an explicit DOMPurify pass on the SVG output as defense-in-depth, or at minimum a comment documenting the safety invariant. [fixable]
| ); | ||
| } | ||
|
|
||
| if (!svg) return null; |
There was a problem hiding this comment.
🔵 style: When svg is null and there's no error (initial render / loading state), the component returns null — a blank gap in the message. Consider rendering a lightweight loading indicator (e.g., a skeleton or the raw code block) so the user sees something while mermaid loads (~1MB async import + render). [fixable]
| <table {...props}>{children}</table> | ||
| </div> | ||
| ), | ||
| pre: ({ children, ...props }) => { |
There was a problem hiding this comment.
🔵 style: The pre handler here renders a plain <pre> for non-mermaid code blocks, while the pre handler in MessageBubble.tsx wraps them with a CopyButton. This is intentional (markdown-config serves MarkdownPreviewCard and FileViewer where CopyButton isn't needed), but the behavioral difference between two nearly identical pre handlers in the same codebase could surprise a future contributor.
dimakis
left a comment
There was a problem hiding this comment.
Centaur Review
Found 5 issue(s) (1 warning).
frontend/src/components/MessageBubble.tsx
Well-structured addition of mermaid diagram support with dynamic imports, strict security, and good test coverage. The useMemo refactor for the preview-card auto-close fix is sound but its dependency on currentPath could still reset child component state on navigation — consider using a ref instead.
- 🟡 regressions: The
useMemoformdComponentscloses overnavigateandcurrentPathin its dependency array, butnavigateandcurrentPathare not listed as explicit props — they come from hooks. IfuseLocation()returns a newsearchstring (e.g. query param change) while the message is streaming, theahandler'scurrentPathwill update and re-create all components mid-stream. This is correct behavior but worth noting: the dependencycurrentPathincludeslocation.search, so any query-param change (even unrelated) will recreate all markdown components, potentially resetting internal state of child components likeMarkdownPreviewCardexpanded state. This partially undermines the stated goal of 'preserving component instances across parent re-renders'. Consider splittingcurrentPathout of the memo or using a ref for the navigation path.[fixable]
frontend/src/components/MermaidBlock.tsx
Well-structured addition of mermaid diagram support with dynamic imports, strict security, and good test coverage. The useMemo refactor for the preview-card auto-close fix is sound but its dependency on currentPath could still reset child component state on navigation — consider using a ref instead.
- 🔵 unsafe_assumptions (L56): The DOM cleanup
document.getElementById(d${id})?.remove()assumes mermaid's internal temp element naming convention (dprefix). This is an undocumented implementation detail of mermaid that could change across versions. Consider wrapping the mermaid render call in a try/finally that queries by the known container pattern, or document the dependency on mermaid internals with a version-pinned comment. - 🔵 style (L7):
_resetMermaidInitis exported solely for test cleanup. Consider usingvi.resetModules()in tests instead, which re-imports the module with fresh state and avoids shipping a test-only export in production code.[fixable]
frontend/src/components/__tests__/MermaidBlock.test.ts
Well-structured addition of mermaid diagram support with dynamic imports, strict security, and good test coverage. The useMemo refactor for the preview-card auto-close fix is sound but its dependency on currentPath could still reset child component state on navigation — consider using a ref instead.
- 🔵 missing_tests: No test covers the cancellation/cleanup path — e.g., unmounting the component while the dynamic import or mermaid.render() is in-flight. A test that renders, immediately unmounts (cleanup), then resolves the mock would verify that
cancelledflag prevents stale state updates.[fixable]
frontend/src/components/__tests__/MessageBubble.test.ts
Well-structured addition of mermaid diagram support with dynamic imports, strict security, and good test coverage. The useMemo refactor for the preview-card auto-close fix is sound but its dependency on currentPath could still reset child component state on navigation — consider using a ref instead.
- 🔵 missing_tests: The new mermaid tests in MessageBubble.test.ts verify that the
precomponent dispatches to MermaidBlock vs code-block-wrapper, but there's no test verifying that theuseMemopreserves component identity across re-renders (the stated purpose of the refactor). A test that re-renders TextBubble and assertscapturedComponentsreferential equality would validate the fix.[fixable]
| setError(null); | ||
| } | ||
| } catch { | ||
| if (!cancelled) { |
There was a problem hiding this comment.
🔵 unsafe_assumptions: The DOM cleanup document.getElementById(d${id})?.remove() assumes mermaid's internal temp element naming convention (d prefix). This is an undocumented implementation detail of mermaid that could change across versions. Consider wrapping the mermaid render call in a try/finally that queries by the known container pattern, or document the dependency on mermaid internals with a version-pinned comment.
| let mermaidInitialized = false; | ||
|
|
||
| /** Exported for test cleanup only. */ | ||
| export function _resetMermaidInit() { |
There was a problem hiding this comment.
🔵 style: _resetMermaidInit is exported solely for test cleanup. Consider using vi.resetModules() in tests instead, which re-imports the module with fresh state and avoids shipping a test-only export in production code. [fixable]
- Use ref for currentPath to prevent useMemo invalidation on navigation - Remove test-only _resetMermaidInit export, use vi.resetModules() instead - Add version comment for mermaid DOM cleanup convention - Add cancellation/cleanup test for unmount during in-flight render - Add useMemo identity preservation test for mdComponents stability - Fix pre-existing anchor test failures (drill into span wrapper) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
/review |
Lint flagged unused import from pre-freshMermaidBlock() approach. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
dimakis
left a comment
There was a problem hiding this comment.
Centaur Review
Found 5 issue(s) (1 warning).
frontend/src/components/__tests__/MermaidBlock.test.ts
Well-structured PR with clean mermaid integration, correct useMemo fix for MarkdownPreviewCard state loss, and good test coverage — only minor issues around a confusing dead import in tests, implicit XSS safety reliance, and a missing integration test for the sanitize schema change.
- 🟡 bugs (L69): The 'only initializes once' test has a dead import on line 69:
const { MermaidBlock } = await import('../MermaidBlock')is executed, populating the module cache, butMermaidBlockis never used. ThenfreshMermaidBlock()callsvi.resetModules()which clears that cache, making the import meaningless. The test works correctly (Fresh and Same come from the same post-reset module instance), but the dead import is confusing and suggests the test was written with a misunderstanding of the module caching mechanics. Remove line 69 for clarity.[fixable]
frontend/src/components/MermaidBlock.tsx
Well-structured PR with clean mermaid integration, correct useMemo fix for MarkdownPreviewCard state loss, and good test coverage — only minor issues around a confusing dead import in tests, implicit XSS safety reliance, and a missing integration test for the sanitize schema change.
- 🔵 unsafe_assumptions (L83): Using
dangerouslySetInnerHTML={{ __html: svg }}with mermaid's rendered SVG. While mermaid v11 withsecurityLevel: 'strict'uses DOMPurify internally to sanitize output (DOMPurify is a transitive dependency visible in the lockfile), this is an implicit safety guarantee tied to mermaid's internals. If mermaid ever changes its sanitization behavior, this becomes an XSS vector. Consider adding a brief comment noting the DOMPurify reliance, or adding an explicit DOMPurify.sanitize() call on the SVG output for defense in depth.[fixable]
frontend/src/lib/__tests__/markdown-config.test.ts
Well-structured PR with clean mermaid integration, correct useMemo fix for MarkdownPreviewCard state loss, and good test coverage — only minor issues around a confusing dead import in tests, implicit XSS safety reliance, and a missing integration test for the sanitize schema change.
- 🔵 missing_tests: The sanitize schema change (adding
code: [..., ['className', /^language-/]]) is not directly tested. While the mermaid detection tests verify the flow works end-to-end, there's no test confirming that rehype-sanitize actually preserveslanguage-mermaidclasses on<code>elements. If the regex or schema structure is wrong, mermaid detection would silently fail in production (classes stripped before reaching theprehandler). An integration test rendering markdown with a mermaid code block through the full rehype pipeline would catch this.[fixable] - 🔵 style (L11): Three
as anycasts are used to callpreas a function. SincemarkdownComponentsis typed asComponents(from react-markdown), thepreproperty is typed as a component rather than a plain function. Consider extracting theprehandler into a named function with explicit types in markdown-config.tsx, or use a type assertion once at theprebinding rather than repeatingas anyin each test.[fixable]
frontend/src/lib/markdown-config.tsx
Well-structured PR with clean mermaid integration, correct useMemo fix for MarkdownPreviewCard state loss, and good test coverage — only minor issues around a confusing dead import in tests, implicit XSS safety reliance, and a missing integration test for the sanitize schema change.
- 🔵 regressions (L30): The
prehandler inmarkdownComponents(used by MarkdownPreviewCard and FileViewer) does not include a CopyButton for non-mermaid code blocks, unlike the equivalent handler in MessageBubble.tsx which wraps code blocks withcode-block-wrapper+CopyButton. This means code blocks inside MarkdownPreviewCard and FileViewer won't have copy buttons. This appears intentional (the old version had noprehandler at all), but worth confirming since the twoprehandlers now diverge in behavior.[fixable]
| // Use the same module instance for both renders (no resetModules) | ||
| const { MermaidBlock } = await import('../MermaidBlock'); | ||
| // Reset init state via fresh module for this test | ||
| const Fresh = await freshMermaidBlock(); |
There was a problem hiding this comment.
🟡 bugs: The 'only initializes once' test has a dead import on line 69: const { MermaidBlock } = await import('../MermaidBlock') is executed, populating the module cache, but MermaidBlock is never used. Then freshMermaidBlock() calls vi.resetModules() which clears that cache, making the import meaningless. The test works correctly (Fresh and Same come from the same post-reset module instance), but the dead import is confusing and suggests the test was written with a misunderstanding of the module caching mechanics. Remove line 69 for clarity. [fixable]
|
|
||
| return ( | ||
| <div className="mermaid-block"> | ||
| <div className="mermaid-block-svg" dangerouslySetInnerHTML={{ __html: svg }} /> |
There was a problem hiding this comment.
🔵 unsafe_assumptions: Using dangerouslySetInnerHTML={{ __html: svg }} with mermaid's rendered SVG. While mermaid v11 with securityLevel: 'strict' uses DOMPurify internally to sanitize output (DOMPurify is a transitive dependency visible in the lockfile), this is an implicit safety guarantee tied to mermaid's internals. If mermaid ever changes its sanitization behavior, this becomes an XSS vector. Consider adding a brief comment noting the DOMPurify reliance, or adding an explicit DOMPurify.sanitize() call on the SVG output for defense in depth. [fixable]
| it('renders MermaidBlock for language-mermaid code blocks', () => { | ||
| const codeEl = createElement('code', { className: 'language-mermaid' }, 'graph TD; A-->B;'); | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| const result = (pre as any)({ children: codeEl }); |
There was a problem hiding this comment.
🔵 style: Three as any casts are used to call pre as a function. Since markdownComponents is typed as Components (from react-markdown), the pre property is typed as a component rather than a plain function. Consider extracting the pre handler into a named function with explicit types in markdown-config.tsx, or use a type assertion once at the pre binding rather than repeating as any in each test. [fixable]
| ), | ||
| pre: ({ children, ...props }) => { | ||
| const mermaidCode = getMermaidCode(children); | ||
| if (mermaidCode !== null) return <MermaidBlock code={mermaidCode} />; |
There was a problem hiding this comment.
🔵 regressions: The pre handler in markdownComponents (used by MarkdownPreviewCard and FileViewer) does not include a CopyButton for non-mermaid code blocks, unlike the equivalent handler in MessageBubble.tsx which wraps code blocks with code-block-wrapper + CopyButton. This means code blocks inside MarkdownPreviewCard and FileViewer won't have copy buttons. This appears intentional (the old version had no pre handler at all), but worth confirming since the two pre handlers now diverge in behavior. [fixable]
|
/review |
Summary
mermaidpackage andMermaidBlockcomponent. Code blocks tagged```mermaidnow render as interactive diagrams (dark theme) instead of raw code. Works in both chat messages and file viewer.componentsobject inTextBubbleviauseMemo. Previously, every parent re-render (voice state, tokens, connection changes) recreated component functions, causing react-markdown to unmount/remount the tree and resetMarkdownPreviewCardexpanded state.Test plan
.mdfiles🤖 Generated with Claude Code