Skip to content

feat(frontend): mermaid diagrams + preview card auto-close fix - #434

Open
dimakis wants to merge 8 commits into
mainfrom
feat/mermaid-and-preview-fix
Open

feat(frontend): mermaid diagrams + preview card auto-close fix#434
dimakis wants to merge 8 commits into
mainfrom
feat/mermaid-and-preview-fix

Conversation

@dimakis

@dimakis dimakis commented Jul 2, 2026

Copy link
Copy Markdown
Owner

Summary

  • Mermaid rendering: Added mermaid package and MermaidBlock component. Code blocks tagged ```mermaid now render as interactive diagrams (dark theme) instead of raw code. Works in both chat messages and file viewer.
  • Preview card auto-close fix: Memoized the components object in TextBubble via useMemo. Previously, every parent re-render (voice state, tokens, connection changes) recreated component functions, causing react-markdown to unmount/remount the tree and reset MarkdownPreviewCard expanded state.

Test plan

  • Send a message containing a mermaid code block — should render as a diagram, not raw code
  • Expand a markdown preview card inline — should stay open indefinitely until manually closed
  • Verify normal code blocks still render with syntax highlighting and copy button
  • Verify file viewer renders mermaid diagrams in .md files
  • Check mermaid fallback: invalid syntax should show raw code block

🤖 Generated with Claude Code

@dimakis dimakis left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 = 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]
  • 🔵 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 has useTheme set 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: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]

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 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]
  • 🔵 style (L15): 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]

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_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]

},
});

let renderCounter = 0;

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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({

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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';

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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 }) => {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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]

Comment thread frontend/src/lib/markdown-config.tsx Outdated
attributes: {
...defaultSchema.attributes,
img: [...(defaultSchema.attributes?.img ?? []), 'width', 'height'],
code: [...(defaultSchema.attributes?.code ?? []), 'className'],

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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':

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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]

dimakis and others added 2 commits July 2, 2026 23:26
…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>
@dimakis
dimakis force-pushed the feat/mermaid-and-preview-fix branch from 00d1cc3 to 01e46b8 Compare July 2, 2026 22:27

@dimakis dimakis left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 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]
  • 🔵 style (L35): 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]
  • 🔵 missing_tests: There are no unit tests for the MermaidBlock component itself — only for the detection/routing logic in the pre handler. The error fallback path (rendering a plain code block), the loading state (svg is null), and the cleanup of leftover DOM elements on render failure are untested. A component-level test (mocking mermaid.render) would cover these branches.
  • 🔵 bugs (L66): 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]
  • 🔵 unsafe_assumptions (L5): 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.

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 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]

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 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]

function ensureMermaidInit() {
if (mermaidInitialized) return;
mermaidInitialized = true;
mermaid.initialize({

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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);

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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]);

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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;

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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 }) => {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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'>) => {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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 dimakis left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 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]

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 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]
  • 🔵 unsafe_assumptions (L5): 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.

expect(html).toBe('');
});

it('renders fallback code block on render error', async () => {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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]

Comment thread frontend/src/lib/mermaid-detect.ts Outdated
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)) {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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'>) => {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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();

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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;

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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 dimakis left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 renderToStaticMarkup which skips useEffect — 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's render + waitFor to 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-custom would 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 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]

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 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]
  • 🔵 style (L5): 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.

Comment thread frontend/src/lib/mermaid-detect.ts Outdated
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)) {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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 }) => {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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();

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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;

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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 dimakis left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 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]

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 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]
  • 🔵 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 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.

);
},
}),
[navigate, currentPath],

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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);

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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;

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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 dimakis left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-block as 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 through markdown-config.test.ts and MessageBubble.test.ts, but given the project's TDD requirement, a mermaid-detect.test.ts covering 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 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]

}
}

/* ===== Mermaid Diagrams ===== */

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.
*/

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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;

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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 dimakis left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 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]

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 like className being an array (possible in some rehype configurations), children being undefined/null, or multiple code children are not covered. A frontend/src/lib/__tests__/mermaid-detect.test.ts would 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';

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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;

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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],

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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 dimakis left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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): 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]
  • 🟡 unsafe_assumptions (L85): 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]
  • 🔵 style (L81): 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]

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 = true cleanup path — e.g., unmounting the component before the async mermaid.render() resolves. An act + cleanup before the mock resolves would verify that setSvg/setError are not called after unmount. [fixable]
  • 🔵 missing_tests: No test covers re-rendering when the code prop changes (the effect re-runs because code is in the dependency array). Verifying that mermaid.render is 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 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.

// the main bundle — only loaded when a mermaid diagram is encountered.
const { default: mermaid } = await import('mermaid');
if (!mermaidInitialized) {
mermaidInitialized = true;

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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 }} />

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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;

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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 }) => {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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 dimakis left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 useMemo for mdComponents closes over navigate and currentPath in its dependency array, but navigate and currentPath are not listed as explicit props — they come from hooks. If useLocation() returns a new search string (e.g. query param change) while the message is streaming, the a handler's currentPath will update and re-create all components mid-stream. This is correct behavior but worth noting: the dependency currentPath includes location.search, so any query-param change (even unrelated) will recreate all markdown components, potentially resetting internal state of child components like MarkdownPreviewCard expanded state. This partially undermines the stated goal of 'preserving component instances across parent re-renders'. Consider splitting currentPath out 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 (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.
  • 🔵 style (L7): _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]

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 cancelled flag 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 pre component dispatches to MermaidBlock vs code-block-wrapper, but there's no test verifying that the useMemo preserves component identity across re-renders (the stated purpose of the refactor). A test that re-renders TextBubble and asserts capturedComponents referential equality would validate the fix. [fixable]

setError(null);
}
} catch {
if (!cancelled) {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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() {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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>
@dimakis

dimakis commented Jul 4, 2026

Copy link
Copy Markdown
Owner Author

/review

Lint flagged unused import from pre-freshMermaidBlock() approach.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@dimakis dimakis left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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, 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]

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 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]

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 preserves language-mermaid classes on <code> elements. If the regex or schema structure is wrong, mermaid detection would silently fail in production (classes stripped before reaching the pre handler). An integration test rendering markdown with a mermaid code block through the full rehype pipeline would catch this. [fixable]
  • 🔵 style (L11): 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]

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 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]

// 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();

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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 }} />

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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 });

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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} />;

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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]

@dimakis

dimakis commented Jul 4, 2026

Copy link
Copy Markdown
Owner Author

/review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant