From 6319b25b56befac75ac7a706fc0c72ee6f27f0ca Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 12 Sep 2026 19:28:18 +0100 Subject: [PATCH] chore(deps): react-markdown 10 react-markdown 10 removed the `className` prop and no longer renders a wrapper element; passing className now throws at render time. Each of the three call sites gets an explicit wrapping div carrying the same classes, which reproduces the v9 DOM exactly. The large diff is re-indentation from the added wrapper. `git diff -w` shows the only substantive change per file is the moved className. Fixes the 19 tests that failed on the upgrade. 598 tests pass, build clean, lint unchanged. --- frontend/package-lock.json | 8 +- frontend/package.json | 2 +- frontend/src/components/ArtifactSidebar.tsx | 191 +++++------ frontend/src/components/MarkdownPreview.tsx | 121 +++---- frontend/src/conversation/MarkdownAnswer.tsx | 341 ++++++++++--------- 5 files changed, 334 insertions(+), 329 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index bae6ee3e..f7858c4c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -36,7 +36,7 @@ "react-force-graph-2d": "^1.29.1", "react-google-drive-picker": "^1.2.2", "react-i18next": "^17.0.13", - "react-markdown": "^9.1.0", + "react-markdown": "^10.1.0", "react-redux": "^9.3.0", "react-router-dom": "^7.18.3", "react-syntax-highlighter": "^16.1.1", @@ -10425,9 +10425,9 @@ } }, "node_modules/react-markdown": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-9.1.0.tgz", - "integrity": "sha512-xaijuJB0kzGiUdG7nc2MOMDUDBWPyGAjZtUrow9XxUeua8IqeP+VlIfAZ3bphpcLTnSZXz6z9jcVC/TCwbfgdw==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", + "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", diff --git a/frontend/package.json b/frontend/package.json index 00a997d0..7e8d2857 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -52,7 +52,7 @@ "react-force-graph-2d": "^1.29.1", "react-google-drive-picker": "^1.2.2", "react-i18next": "^17.0.13", - "react-markdown": "^9.1.0", + "react-markdown": "^10.1.0", "react-redux": "^9.3.0", "react-router-dom": "^7.18.3", "react-syntax-highlighter": "^16.1.1", diff --git a/frontend/src/components/ArtifactSidebar.tsx b/frontend/src/components/ArtifactSidebar.tsx index 5788bb01..fe3ddaaf 100644 --- a/frontend/src/components/ArtifactSidebar.tsx +++ b/frontend/src/components/ArtifactSidebar.tsx @@ -165,105 +165,106 @@ function NoteView({ data }: { data: NoteArtifactData }) {
{data.content ? ( - + -
- - {language} - - + return match ? ( +
+
+ + {language} + + +
+ + {String(children).replace(/\n$/, '')} +
- - {String(children).replace(/\n$/, '')} - -
- ) : ( - - {children} - - ); - }, - ul({ children }) { - return ( -
    - {children} -
- ); - }, - ol({ children }) { - return ( -
    - {children} -
- ); - }, - a({ children, href }) { - return ( - - {children} - - ); - }, - p({ children }) { - return

{children}

; - }, - h1({ children }) { - return

{children}

; - }, - h2({ children }) { - return

{children}

; - }, - h3({ children }) { - return

{children}

; - }, - blockquote({ children }) { - return ( -
- {children} -
- ); - }, - }} - > - {data.content} -
+ {children} + + ); + }, + ul({ children }) { + return ( +
    + {children} +
+ ); + }, + ol({ children }) { + return ( +
    + {children} +
+ ); + }, + a({ children, href }) { + return ( + + {children} + + ); + }, + p({ children }) { + return

{children}

; + }, + h1({ children }) { + return

{children}

; + }, + h2({ children }) { + return

{children}

; + }, + h3({ children }) { + return

{children}

; + }, + blockquote({ children }) { + return ( +
+ {children} +
+ ); + }, + }} + > + {data.content} +
+
) : (

Empty note

)} diff --git a/frontend/src/components/MarkdownPreview.tsx b/frontend/src/components/MarkdownPreview.tsx index 4a2634e5..d671e8e1 100644 --- a/frontend/src/components/MarkdownPreview.tsx +++ b/frontend/src/components/MarkdownPreview.tsx @@ -9,66 +9,67 @@ import remarkGfm from 'remark-gfm'; export default function MarkdownPreview({ content }: { content: string }) { return (
- - {children} - - ); - }, - ul({ children }) { - return ( -
    - {children} -
- ); - }, - ol({ children }) { - return ( -
    - {children} -
- ); - }, - h1({ children }) { - return

{children}

; - }, - h2({ children }) { - return

{children}

; - }, - h3({ children }) { - return

{children}

; - }, - code({ children, ...rest }) { - return ( - - {children} - - ); - }, - blockquote({ children }) { - return ( -
- {children} -
- ); - }, - }} - > - {content} -
+
+ + {children} + + ); + }, + ul({ children }) { + return ( +
    + {children} +
+ ); + }, + ol({ children }) { + return ( +
    + {children} +
+ ); + }, + h1({ children }) { + return

{children}

; + }, + h2({ children }) { + return

{children}

; + }, + h3({ children }) { + return

{children}

; + }, + code({ children, ...rest }) { + return ( + + {children} + + ); + }, + blockquote({ children }) { + return ( +
+ {children} +
+ ); + }, + }} + > + {content} +
+
); } diff --git a/frontend/src/conversation/MarkdownAnswer.tsx b/frontend/src/conversation/MarkdownAnswer.tsx index b85e58fb..9d41dcd0 100644 --- a/frontend/src/conversation/MarkdownAnswer.tsx +++ b/frontend/src/conversation/MarkdownAnswer.tsx @@ -184,177 +184,180 @@ export default function MarkdownAnswer({ {contentSegments.map((segment, index) => ( {segment.type === 'text' ? ( - {children}; - } - if (sandboxLink.kind === 'artifact') { - return renderArtifactChip(sandboxLink.artifact, children); - } - if (href?.startsWith('#cite-')) { - const num = href.replace('#cite-', ''); - const sourceIdx = parseInt(num, 10) - 1; - return ( - - ); - } - return ( - - {children} - - ); - }, - img({ src, alt }) { - // `![chart](sandbox:/mnt/data/chart.png)` is how a model - // announces a plot it produced. react-markdown runs - // urlTransform over `src` too, so without this the invented - // scheme survives and renders a broken-image box beside the - // chip that opens the very same file. - const source = typeof src === 'string' ? src : undefined; - const sandboxLink = resolveSandboxLink( - source, - artifacts, - turnArtifacts, - ); - if (sandboxLink.kind === 'artifact') { - const { artifact } = sandboxLink; - return renderArtifactChip( - artifact, - alt || artifact.label || 'Open file', - ); - } - if (sandboxLink.kind === 'plain') { - return <>{alt ?? ''}; - } - return {alt}; - }, - code(props) { - const { children, className, node, ref, ...rest } = props; - const match = /language-(\w+)/.exec(className || ''); - const language = match ? match[1] : ''; - - return match ? ( -
-
- - {language} - - -
- - {String(children).replace(/\n$/, '')} - -
- ) : ( - - {children} - - ); - }, - ul({ children }) { - return ( -
    - {children} -
- ); - }, - ol({ children }) { - return ( -
    - {children} -
- ); - }, - table({ children }) { - return ( -
- + if (el) { + el.scrollIntoView({ + behavior: 'smooth', + block: 'center', + }); + el.classList.add('ring-2', 'ring-purple-500'); + setTimeout( + () => + el.classList.remove( + 'ring-2', + 'ring-purple-500', + ), + 2000, + ); + } + }} + className="mx-0.5 h-5 min-w-5 rounded-full bg-purple-100 px-1.5 text-xs font-semibold text-purple-700 hover:bg-purple-200 dark:bg-purple-900/40 dark:text-purple-300 dark:hover:bg-purple-900/60" + title={`Jump to source ${num}`} + > + {num} + + ); + } + return ( + {children} -
-
- ); - }, - thead({ children }) { - return ( - - {children} - - ); - }, - tr({ children }) { - return ( - - {children} - - ); - }, - th({ children }) { - return {children}; - }, - td({ children }) { - return {children}; - }, - }} - > - {segment.content} -
+ + ); + }, + img({ src, alt }) { + // `![chart](sandbox:/mnt/data/chart.png)` is how a model + // announces a plot it produced. react-markdown runs + // urlTransform over `src` too, so without this the invented + // scheme survives and renders a broken-image box beside the + // chip that opens the very same file. + const source = typeof src === 'string' ? src : undefined; + const sandboxLink = resolveSandboxLink( + source, + artifacts, + turnArtifacts, + ); + if (sandboxLink.kind === 'artifact') { + const { artifact } = sandboxLink; + return renderArtifactChip( + artifact, + alt || artifact.label || 'Open file', + ); + } + if (sandboxLink.kind === 'plain') { + return <>{alt ?? ''}; + } + return ( + {alt} + ); + }, + code(props) { + const { children, className, node, ref, ...rest } = props; + const match = /language-(\w+)/.exec(className || ''); + const language = match ? match[1] : ''; + + return match ? ( +
+
+ + {language} + + +
+ + {String(children).replace(/\n$/, '')} + +
+ ) : ( + + {children} + + ); + }, + ul({ children }) { + return ( + + ); + }, + ol({ children }) { + return ( +
    + {children} +
+ ); + }, + table({ children }) { + return ( +
+ + {children} +
+
+ ); + }, + thead({ children }) { + return ( + + {children} + + ); + }, + tr({ children }) { + return ( + + {children} + + ); + }, + th({ children }) { + return {children}; + }, + td({ children }) { + return {children}; + }, + }} + > + {segment.content} + + ) : (