From e6bfa78e92acac8142ef6aec2e971e12eea19aa8 Mon Sep 17 00:00:00 2001 From: ibelick Date: Fri, 13 Feb 2026 08:42:23 +0100 Subject: [PATCH] feat: replace react-markdown with streamdown --- apps/webclaw/package.json | 5 +- .../src/components/prompt-kit/markdown.tsx | 77 +++---- apps/webclaw/src/styles.css | 1 + pnpm-lock.yaml | 191 +++++++++++++----- 4 files changed, 166 insertions(+), 108 deletions(-) diff --git a/apps/webclaw/package.json b/apps/webclaw/package.json index a4da7d0..bc931e4 100644 --- a/apps/webclaw/package.json +++ b/apps/webclaw/package.json @@ -24,15 +24,12 @@ "@tanstack/router-plugin": "^1.132.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "marked": "^17.0.1", "motion": "^12.29.2", "nitro": "npm:nitro-nightly@latest", "react": "^19.2.0", "react-dom": "^19.2.0", - "react-markdown": "^10.1.0", - "remark-breaks": "^4.0.0", - "remark-gfm": "^4.0.1", "shiki": "^3.21.0", + "streamdown": "^2.2.0", "tailwind-merge": "^3.4.0", "tailwindcss": "^4.1.18", "use-stick-to-bottom": "^1.1.2", diff --git a/apps/webclaw/src/components/prompt-kit/markdown.tsx b/apps/webclaw/src/components/prompt-kit/markdown.tsx index 2517c03..30b006b 100644 --- a/apps/webclaw/src/components/prompt-kit/markdown.tsx +++ b/apps/webclaw/src/components/prompt-kit/markdown.tsx @@ -1,22 +1,13 @@ -import { marked } from 'marked' -import { memo, useId, useMemo } from 'react' -import ReactMarkdown from 'react-markdown' -import remarkBreaks from 'remark-breaks' -import remarkGfm from 'remark-gfm' +import { memo } from 'react' +import { Streamdown } from 'streamdown' import { CodeBlock } from './code-block' -import type { Components } from 'react-markdown' import { cn } from '@/lib/utils' export type MarkdownProps = { children: string id?: string className?: string - components?: Partial -} - -function parseMarkdownIntoBlocks(markdown: string): Array { - const tokens = marked.lexer(markdown) - return tokens.map((token) => token.raw) + components?: Record> } function extractLanguage(className?: string): string { @@ -25,7 +16,7 @@ function extractLanguage(className?: string): string { return match ? match[1] : 'text' } -const INITIAL_COMPONENTS: Partial = { +const INITIAL_COMPONENTS: Record> = { code: function CodeComponent({ className, children }) { const isInline = !className?.includes('language-') @@ -50,13 +41,23 @@ const INITIAL_COMPONENTS: Partial = { return <>{children} }, h1: function H1Component({ children }) { - return

{children}

+ return ( +

+ {children} +

+ ) }, h2: function H2Component({ children }) { - return

{children}

+ return ( +

+ {children} +

+ ) }, h3: function H3Component({ children }) { - return

{children}

+ return ( +

{children}

+ ) }, p: function PComponent({ children }) { return ( @@ -142,50 +143,18 @@ const INITIAL_COMPONENTS: Partial = { }, } -const MemoizedMarkdownBlock = memo( - function MarkdownBlock({ - content, - components = INITIAL_COMPONENTS, - }: { - content: string - components?: Partial - }) { - return ( - - {content} - - ) - }, - function propsAreEqual(prevProps, nextProps) { - return prevProps.content === nextProps.content - }, -) - -MemoizedMarkdownBlock.displayName = 'MemoizedMarkdownBlock' - function MarkdownComponent({ children, - id, className, components = INITIAL_COMPONENTS, }: MarkdownProps) { - const generatedId = useId() - const blockId = id ?? generatedId - const blocks = useMemo(() => parseMarkdownIntoBlocks(children), [children]) - return ( -
- {blocks.map((block, index) => ( - - ))} -
+ + {children} + ) } diff --git a/apps/webclaw/src/styles.css b/apps/webclaw/src/styles.css index 0637490..acbbf06 100644 --- a/apps/webclaw/src/styles.css +++ b/apps/webclaw/src/styles.css @@ -1,5 +1,6 @@ @import url('https://fonts.googleapis.com/css2?family=EB+Garamond:opsz,wght@8..120,400..800&family=JetBrains+Mono:wght@400;500&display=swap'); @import 'tailwindcss'; +@source "../../../node_modules/streamdown/dist/*.js"; .root { isolation: isolate; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ea135a1..39ed1f9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -87,9 +87,6 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 - marked: - specifier: ^17.0.1 - version: 17.0.1 motion: specifier: ^12.29.2 version: 12.33.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -102,18 +99,12 @@ importers: react-dom: specifier: ^19.2.0 version: 19.2.4(react@19.2.4) - react-markdown: - specifier: ^10.1.0 - version: 10.1.0(@types/react@19.2.13)(react@19.2.4) - remark-breaks: - specifier: ^4.0.0 - version: 4.0.0 - remark-gfm: - specifier: ^4.0.1 - version: 4.0.1 shiki: specifier: ^3.21.0 version: 3.22.0 + streamdown: + specifier: ^2.2.0 + version: 2.2.0(react@19.2.4) tailwind-merge: specifier: ^3.4.0 version: 3.4.0 @@ -2315,15 +2306,33 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + hast-util-from-parse5@8.0.3: + resolution: {integrity: sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==} + + hast-util-parse-selector@4.0.0: + resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==} + + hast-util-raw@9.1.0: + resolution: {integrity: sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==} + + hast-util-sanitize@5.0.2: + resolution: {integrity: sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg==} + hast-util-to-html@9.0.5: resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} hast-util-to-jsx-runtime@2.3.6: resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} + hast-util-to-parse5@8.0.1: + resolution: {integrity: sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA==} + hast-util-whitespace@3.0.0: resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + hastscript@9.0.1: + resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==} + html-encoding-sniffer@6.0.0: resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} @@ -2655,9 +2664,6 @@ packages: mdast-util-mdxjs-esm@2.0.1: resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==} - mdast-util-newline-to-break@2.0.0: - resolution: {integrity: sha512-MbgeFca0hLYIEx/2zGsszCSEJJ1JSCdiY5xQxRcLDDGa8EPvlLPupJ4DSajbMPAnC0je8jfb9TiUATnxxrHUog==} - mdast-util-phrasing@4.1.0: resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} @@ -2942,12 +2948,6 @@ packages: react-is@17.0.2: resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} - react-markdown@10.1.0: - resolution: {integrity: sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==} - peerDependencies: - '@types/react': '>=18' - react: '>=18' - react-refresh@0.18.0: resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} engines: {node: '>=0.10.0'} @@ -2973,8 +2973,14 @@ packages: regex@6.1.0: resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==} - remark-breaks@4.0.0: - resolution: {integrity: sha512-IjEjJOkH4FuJvHZVIW0QCDWxcG96kCq7An/KVH2NfJe6rKZU2AsHeB3OEjPNRxi4QC34Xdx7I2KGYn6IpT7gxQ==} + rehype-harden@1.1.7: + resolution: {integrity: sha512-j5DY0YSK2YavvNGV+qBHma15J9m0WZmRe8posT5AtKDS6TNWtMVTo6RiqF8SidfcASYz8f3k2J/1RWmq5zTXUw==} + + rehype-raw@7.0.0: + resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==} + + rehype-sanitize@6.0.0: + resolution: {integrity: sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg==} remark-gfm@4.0.1: resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} @@ -2988,6 +2994,9 @@ packages: remark-stringify@11.0.0: resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + remend@1.2.0: + resolution: {integrity: sha512-NbKrdWweTRuByPYErzQCNpNtsR9M1QQ0hK2UzmnmlSaEqHnkQ5Korlyi8KpdbOJ0rImJfRy4EAY0uDxYnL9Plw==} + require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} @@ -3097,6 +3106,11 @@ packages: std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + streamdown@2.2.0: + resolution: {integrity: sha512-Y51o1I/sjpAy4Yn7j7R4TbUl9gcUZ7BTrHS+68IhrUBoYpNQZ28z06vww1MBFu4mSwvgF8xQIxIH2b9S9IHDyQ==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + stringify-entities@4.0.4: resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} @@ -3370,6 +3384,9 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + vfile-location@5.0.3: + resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==} + vfile-message@4.0.3: resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} @@ -3472,6 +3489,9 @@ packages: resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} engines: {node: '>=18'} + web-namespaces@2.0.1: + resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} + web-vitals@5.1.0: resolution: {integrity: sha512-ArI3kx5jI0atlTtmV0fWU3fjpLmq/nD3Zr1iFFlJLaqa5wLBkUSzINwBPySCX/8jRyjlmy1Volw1kz1g9XE4Jg==} @@ -5598,6 +5618,43 @@ snapshots: has-flag@4.0.0: {} + hast-util-from-parse5@8.0.3: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + devlop: 1.1.0 + hastscript: 9.0.1 + property-information: 7.1.0 + vfile: 6.0.3 + vfile-location: 5.0.3 + web-namespaces: 2.0.1 + + hast-util-parse-selector@4.0.0: + dependencies: + '@types/hast': 3.0.4 + + hast-util-raw@9.1.0: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + '@ungap/structured-clone': 1.3.0 + hast-util-from-parse5: 8.0.3 + hast-util-to-parse5: 8.0.1 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.1 + parse5: 7.3.0 + unist-util-position: 5.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + + hast-util-sanitize@5.0.2: + dependencies: + '@types/hast': 3.0.4 + '@ungap/structured-clone': 1.3.0 + unist-util-position: 5.0.0 + hast-util-to-html@9.0.5: dependencies: '@types/hast': 3.0.4 @@ -5632,10 +5689,28 @@ snapshots: transitivePeerDependencies: - supports-color + hast-util-to-parse5@8.0.1: + dependencies: + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + hast-util-whitespace@3.0.0: dependencies: '@types/hast': 3.0.4 + hastscript@9.0.1: + dependencies: + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + hast-util-parse-selector: 4.0.0 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + html-encoding-sniffer@6.0.0: dependencies: '@exodus/bytes': 1.11.0 @@ -6012,11 +6087,6 @@ snapshots: transitivePeerDependencies: - supports-color - mdast-util-newline-to-break@2.0.0: - dependencies: - '@types/mdast': 4.0.4 - mdast-util-find-and-replace: 3.0.2 - mdast-util-phrasing@4.1.0: dependencies: '@types/mdast': 4.0.4 @@ -6448,24 +6518,6 @@ snapshots: react-is@17.0.2: {} - react-markdown@10.1.0(@types/react@19.2.13)(react@19.2.4): - dependencies: - '@types/hast': 3.0.4 - '@types/mdast': 4.0.4 - '@types/react': 19.2.13 - devlop: 1.1.0 - hast-util-to-jsx-runtime: 2.3.6 - html-url-attributes: 3.0.1 - mdast-util-to-hast: 13.2.1 - react: 19.2.4 - remark-parse: 11.0.0 - remark-rehype: 11.1.2 - unified: 11.0.5 - unist-util-visit: 5.1.0 - vfile: 6.0.3 - transitivePeerDependencies: - - supports-color - react-refresh@0.18.0: {} react@19.2.4: {} @@ -6492,11 +6544,20 @@ snapshots: dependencies: regex-utilities: 2.3.0 - remark-breaks@4.0.0: + rehype-harden@1.1.7: dependencies: - '@types/mdast': 4.0.4 - mdast-util-newline-to-break: 2.0.0 - unified: 11.0.5 + unist-util-visit: 5.1.0 + + rehype-raw@7.0.0: + dependencies: + '@types/hast': 3.0.4 + hast-util-raw: 9.1.0 + vfile: 6.0.3 + + rehype-sanitize@6.0.0: + dependencies: + '@types/hast': 3.0.4 + hast-util-sanitize: 5.0.2 remark-gfm@4.0.1: dependencies: @@ -6532,6 +6593,8 @@ snapshots: mdast-util-to-markdown: 2.1.2 unified: 11.0.5 + remend@1.2.0: {} + require-from-string@2.0.2: {} reselect@5.1.1: {} @@ -6678,6 +6741,27 @@ snapshots: std-env@3.10.0: {} + streamdown@2.2.0(react@19.2.4): + dependencies: + clsx: 2.1.1 + hast-util-to-jsx-runtime: 2.3.6 + html-url-attributes: 3.0.1 + marked: 17.0.1 + react: 19.2.4 + rehype-harden: 1.1.7 + rehype-raw: 7.0.0 + rehype-sanitize: 6.0.0 + remark-gfm: 4.0.1 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + remend: 1.2.0 + tailwind-merge: 3.4.0 + unified: 11.0.5 + unist-util-visit: 5.1.0 + unist-util-visit-parents: 6.0.2 + transitivePeerDependencies: + - supports-color + stringify-entities@4.0.4: dependencies: character-entities-html4: 2.1.0 @@ -6893,6 +6977,11 @@ snapshots: dependencies: react: 19.2.4 + vfile-location@5.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile: 6.0.3 + vfile-message@4.0.3: dependencies: '@types/unist': 3.0.3 @@ -7012,6 +7101,8 @@ snapshots: dependencies: xml-name-validator: 5.0.0 + web-namespaces@2.0.1: {} + web-vitals@5.1.0: {} webidl-conversions@8.0.1: {}