diff --git a/bun.lock b/bun.lock index 05cf7c3e..619335a7 100644 --- a/bun.lock +++ b/bun.lock @@ -146,7 +146,6 @@ "@radix-ui/react-toggle": "^1.1.10", "@radix-ui/react-tooltip": "^1.2.8", "@remixicon/react": "^4.7.0", - "@streamdown/code": "^1.0.2", "@tanstack/react-virtual": "^3.13.18", "@types/react-syntax-highlighter": "^15.5.13", "beautiful-mermaid": "^1.1.3", @@ -154,12 +153,15 @@ "clsx": "^2.1.1", "cmdk": "^1.1.1", "codemirror-lang-elixir": "^4.0.0", + "dompurify": "^3.2.7", "express": "^5.1.0", "fuse.js": "^7.1.0", "ghostty-web": "^0.4.0", "heic2any": "^0.0.4", "html-to-image": "^1.11.13", "http-proxy-middleware": "^3.0.5", + "marked": "^17.0.3", + "morphdom": "^2.7.7", "motion": "^12.23.24", "next-themes": "^0.4.6", "prismjs": "^1.30.0", @@ -167,9 +169,9 @@ "react": "^19.1.1", "react-dom": "^19.1.1", "react-syntax-highlighter": "^15.6.6", + "remend": "^1.2.1", "simple-git": "^3.28.0", "sonner": "^2.0.7", - "streamdown": "^2.2.0", "strip-json-comments": "^5.0.3", "tailwind-merge": "^3.3.1", "yaml": "^2.8.1", @@ -1027,8 +1029,6 @@ "@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@2.3.0", "", {}, "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg=="], - "@streamdown/code": ["@streamdown/code@1.0.3", "", { "dependencies": { "shiki": "^3.19.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-3Ym5TCLcGhrHY2qBaUVWpqNRtxnZvqh4Y5Qm/pTIKA4AmEWwAAoYjZnxG7mOsvOpWVWiDwETjUtchNL1XzQEAw=="], - "@surma/rollup-plugin-off-main-thread": ["@surma/rollup-plugin-off-main-thread@2.2.3", "", { "dependencies": { "ejs": "^3.1.6", "json5": "^2.2.0", "magic-string": "^0.25.0", "string.prototype.matchall": "^4.0.6" } }, "sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ=="], "@swc/helpers": ["@swc/helpers@0.5.19", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-QamiFeIK3txNjgUTNppE6MiG3p7TdninpZu0E0PbqVh1a9FNLT2FRhisaa4NcaX52XVhA5l7Pk58Ft7Sqi/2sA=="], @@ -1461,6 +1461,8 @@ "domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="], + "dompurify": ["dompurify@3.3.3", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA=="], + "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="], "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], @@ -1669,20 +1671,12 @@ "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], - "hast-util-from-parse5": ["hast-util-from-parse5@8.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "hastscript": "^9.0.0", "property-information": "^7.0.0", "vfile": "^6.0.0", "vfile-location": "^5.0.0", "web-namespaces": "^2.0.0" } }, "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg=="], - "hast-util-parse-selector": ["hast-util-parse-selector@2.2.5", "", {}, "sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ=="], - "hast-util-raw": ["hast-util-raw@9.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "hast-util-from-parse5": "^8.0.0", "hast-util-to-parse5": "^8.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "parse5": "^7.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw=="], - - "hast-util-sanitize": ["hast-util-sanitize@5.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "unist-util-position": "^5.0.0" } }, "sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg=="], - "hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="], "hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="], - "hast-util-to-parse5": ["hast-util-to-parse5@8.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA=="], - "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="], "hastscript": ["hastscript@6.0.0", "", { "dependencies": { "@types/hast": "^2.0.0", "comma-separated-tokens": "^1.0.0", "hast-util-parse-selector": "^2.0.0", "property-information": "^5.0.0", "space-separated-tokens": "^1.0.0" } }, "sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w=="], @@ -2073,6 +2067,8 @@ "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="], + "morphdom": ["morphdom@2.7.8", "", {}, "sha512-D/fR4xgGUyVRbdMGU6Nejea1RFzYxYtyurG4Fbv2Fi/daKlWKuXGLOdXtl+3eIwL110cI2hz1ZojGICjjFLgTg=="], + "motion": ["motion@12.34.3", "", { "dependencies": { "framer-motion": "^12.34.3", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-xZIkBGO7v/Uvm+EyaqYd+9IpXu0sZqLywVlGdCFrrMiaO9JI4Kx51mO9KlHSWwll+gZUVY5OJsWgYI5FywJ/tw=="], "motion-dom": ["motion-dom@12.34.3", "", { "dependencies": { "motion-utils": "^12.29.2" } }, "sha512-sYgFe+pR9aIM7o4fhs2aXtOI+oqlUd33N9Yoxcgo1Fv7M20sRkHtCmzE/VRNIcq7uNJ+qio+Xubt1FXH3pQ+eQ=="], @@ -2281,12 +2277,6 @@ "regjsparser": ["regjsparser@0.13.0", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q=="], - "rehype-harden": ["rehype-harden@1.1.8", "", { "dependencies": { "unist-util-visit": "^5.0.0" } }, "sha512-Qn7vR1xrf6fZCrkm9TDWi/AB4ylrHy+jqsNm1EHOAmbARYA6gsnVJBq/sdBh6kmT4NEZxH5vgIjrscefJAOXcw=="], - - "rehype-raw": ["rehype-raw@7.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-raw": "^9.0.0", "vfile": "^6.0.0" } }, "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww=="], - - "rehype-sanitize": ["rehype-sanitize@6.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-sanitize": "^5.0.0" } }, "sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg=="], - "remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="], "remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="], @@ -2417,8 +2407,6 @@ "stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="], - "streamdown": ["streamdown@2.3.0", "", { "dependencies": { "clsx": "^2.1.1", "hast-util-to-jsx-runtime": "^2.3.6", "html-url-attributes": "^3.0.1", "marked": "^17.0.1", "rehype-harden": "^1.1.8", "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.1", "tailwind-merge": "^3.4.0", "unified": "^11.0.5", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-OqS3by/lt91lSicE8RQP2nTsYI6Q/dQgGP2vcyn9YesCmRHhNjswAuBAZA1z0F4+oBU3II/eV51LqjCqwTb1lw=="], - "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "string.prototype.matchall": ["string.prototype.matchall@4.0.12", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "regexp.prototype.flags": "^1.5.3", "set-function-name": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA=="], @@ -2597,8 +2585,6 @@ "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], - "vfile-location": ["vfile-location@5.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg=="], - "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], "vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="], @@ -2607,8 +2593,6 @@ "w3c-keyname": ["w3c-keyname@2.2.8", "", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="], - "web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="], - "web-push": ["web-push@3.6.7", "", { "dependencies": { "asn1.js": "^5.3.0", "http_ece": "1.2.0", "https-proxy-agent": "^7.0.0", "jws": "^4.0.0", "minimist": "^1.2.5" }, "bin": { "web-push": "src/cli.js" } }, "sha512-OpiIUe8cuGjrj3mMBFWY+e4MMIkW3SVT+7vEIjvD9kejGUypv8GPDf84JdPWskK8zMRIJ6xYGm+Kxr8YkPyA0A=="], "web-streams-polyfill": ["web-streams-polyfill@4.0.0-beta.3", "", {}, "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug=="], @@ -2813,8 +2797,6 @@ "globby/slash": ["slash@5.1.0", "", {}, "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg=="], - "hast-util-from-parse5/hastscript": ["hastscript@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="], - "hastscript/@types/hast": ["@types/hast@2.3.10", "", { "dependencies": { "@types/unist": "^2" } }, "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw=="], "hastscript/comma-separated-tokens": ["comma-separated-tokens@1.0.8", "", {}, "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw=="], @@ -2911,8 +2893,6 @@ "glob/minimatch/brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="], - "hast-util-from-parse5/hastscript/hast-util-parse-selector": ["hast-util-parse-selector@4.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A=="], - "hastscript/@types/hast/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], "mdast-util-mdx-jsx/parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], diff --git a/packages/ui/package.json b/packages/ui/package.json index 38449fbe..0b7a24d7 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -52,7 +52,6 @@ "@radix-ui/react-toggle": "^1.1.10", "@radix-ui/react-tooltip": "^1.2.8", "@remixicon/react": "^4.7.0", - "@streamdown/code": "^1.0.2", "@tanstack/react-virtual": "^3.13.18", "@types/react-syntax-highlighter": "^15.5.13", "beautiful-mermaid": "^1.1.3", @@ -60,6 +59,7 @@ "clsx": "^2.1.1", "cmdk": "^1.1.1", "codemirror-lang-elixir": "^4.0.0", + "dompurify": "^3.2.7", "express": "^5.1.0", "fuse.js": "^7.1.0", "ghostty-web": "^0.4.0", @@ -67,15 +67,17 @@ "html-to-image": "^1.11.13", "http-proxy-middleware": "^3.0.5", "motion": "^12.23.24", + "morphdom": "^2.7.7", "next-themes": "^0.4.6", "prismjs": "^1.30.0", "qrcode": "^1.5.4", "react": "^19.1.1", "react-dom": "^19.1.1", "react-syntax-highlighter": "^15.6.6", + "remend": "^1.2.1", "simple-git": "^3.28.0", + "marked": "^17.0.3", "sonner": "^2.0.7", - "streamdown": "^2.2.0", "strip-json-comments": "^5.0.3", "tailwind-merge": "^3.3.1", "yaml": "^2.8.1", diff --git a/packages/ui/src/components/chat/MarkdownRenderer.tsx b/packages/ui/src/components/chat/MarkdownRenderer.tsx index 8779546c..e16ffc5e 100644 --- a/packages/ui/src/components/chat/MarkdownRenderer.tsx +++ b/packages/ui/src/components/chat/MarkdownRenderer.tsx @@ -1,20 +1,22 @@ import React from 'react'; -import { Streamdown } from 'streamdown'; -import { createCodePlugin } from '@streamdown/code'; import { renderMermaidASCII, renderMermaidSVG } from 'beautiful-mermaid'; -import 'streamdown/styles.css'; +import ReactMarkdown from 'react-markdown'; +import type { Components } from 'react-markdown'; +import remarkGfm from 'remark-gfm'; +import { marked, type Tokens } from 'marked'; +import remend from 'remend'; import { FadeInOnReveal } from './message/FadeInOnReveal'; import type { Part } from '@opencode-ai/sdk/v2'; import { cn } from '@/lib/utils'; import { RiFileCopyLine, RiCheckLine, RiDownloadLine } from '@remixicon/react'; +import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { toast } from '@/components/ui'; import { copyTextToClipboard } from '@/lib/clipboard'; -import { isVSCodeRuntime } from '@/lib/desktop'; import { isExternalHttpUrl, openExternalUrl } from '@/lib/url'; import { useOptionalThemeSystem } from '@/contexts/useThemeSystem'; -import { getStreamdownThemePair } from '@/lib/shiki/appThemeRegistry'; import { getDefaultTheme } from '@/lib/theme/themes'; +import { generateSyntaxTheme } from '@/lib/theme/syntaxThemeGenerator'; import type { ToolPopupContent } from './message/types'; import { useUIStore } from '@/stores/useUIStore'; import { useDeviceInfo } from '@/lib/device'; @@ -22,126 +24,6 @@ import { useEffectiveDirectory } from '@/hooks/useEffectiveDirectory'; import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs'; import type { EditorAPI } from '@/lib/api/types'; -const withStableStringId = (value: T, id: string): T => { - const existingPrimitive = (value as Record)[Symbol.toPrimitive]; - if (typeof existingPrimitive === 'function') { - try { - if ((existingPrimitive as () => unknown)() === id) { - return value; - } - } catch { - // Ignore and attempt to define below. - } - } - - try { - Object.defineProperty(value, 'toString', { - value: () => id, - enumerable: false, - configurable: true, - }); - } catch { - // Ignore if non-configurable or frozen. - } - - try { - Object.defineProperty(value, Symbol.toPrimitive, { - value: () => id, - enumerable: false, - configurable: true, - }); - } catch { - // Ignore if non-configurable or frozen. - } - - return value; -}; - -const useMarkdownShikiThemes = (): readonly [string | object, string | object] => { - const themeSystem = useOptionalThemeSystem(); - - const isVSCode = isVSCodeRuntime() && typeof window !== 'undefined'; - - const fallbackLight = getDefaultTheme(false); - const fallbackDark = getDefaultTheme(true); - - const lightThemeId = themeSystem?.lightThemeId ?? fallbackLight.metadata.id; - const darkThemeId = themeSystem?.darkThemeId ?? fallbackDark.metadata.id; - - const lightTheme = - themeSystem?.availableThemes.find((theme) => theme.metadata.id === lightThemeId) ?? - fallbackLight; - const darkTheme = - themeSystem?.availableThemes.find((theme) => theme.metadata.id === darkThemeId) ?? - fallbackDark; - - const fallbackThemes = React.useMemo( - () => getStreamdownThemePair(lightTheme, darkTheme), - [darkTheme, lightTheme], - ); - - const getThemes = React.useCallback((): readonly [string | object, string | object] => { - if (!isVSCode) { - return fallbackThemes; - } - - const provided = window.__OPENCHAMBER_VSCODE_SHIKI_THEMES__; - if (provided?.light && provided?.dark) { - const light = withStableStringId( - { ...(provided.light as Record) }, - `vscode-shiki-light:${String((provided.light as { name?: unknown })?.name ?? 'theme')}`, - ); - const dark = withStableStringId( - { ...(provided.dark as Record) }, - `vscode-shiki-dark:${String((provided.dark as { name?: unknown })?.name ?? 'theme')}`, - ); - return [light, dark] as const; - } - - return fallbackThemes; - }, [fallbackThemes, isVSCode]); - - const [themes, setThemes] = React.useState(getThemes); - - React.useEffect(() => { - if (!isVSCode) { - return; - } - - setThemes(getThemes()); - }, [getThemes, isVSCode]); - - React.useEffect(() => { - if (!isVSCode) return; - - const handler = (event: Event) => { - // Rely on the canonical `window.__OPENCHAMBER_VSCODE_SHIKI_THEMES__` that the webview updates - // before dispatching this event, so we always apply stable cache keys and avoid stale token reuse. - void event; - setThemes(getThemes()); - }; - - window.addEventListener('openchamber:vscode-shiki-themes', handler as EventListener); - return () => window.removeEventListener('openchamber:vscode-shiki-themes', handler as EventListener); - }, [getThemes, isVSCode]); - - return isVSCode ? themes : fallbackThemes; -}; - -type StreamdownCodeThemes = NonNullable[0]>['themes']; - -const useStreamdownPlugins = (shikiThemes: readonly [string | object, string | object]) => { - return React.useMemo( - () => ({ - code: createCodePlugin({ - // Streamdown code plugin runtime accepts theme objects, but current type only models bundled theme names. - themes: shikiThemes as unknown as StreamdownCodeThemes, - }), - }), - [shikiThemes], - ); -}; - const useCurrentMermaidTheme = () => { const themeSystem = useOptionalThemeSystem(); const fallbackLight = getDefaultTheme(false); @@ -425,13 +307,13 @@ const TableWrapper: React.FC<{ children?: React.ReactNode; className?: string }> const tableRef = React.useRef(null); return ( -
+
-
- +
+
{children}
@@ -439,27 +321,6 @@ const TableWrapper: React.FC<{ children?: React.ReactNode; className?: string }> ); }; -type CodeBlockWrapperProps = React.HTMLAttributes & { - children?: React.ReactNode; -}; - -const getMermaidInfo = (children: React.ReactNode): { isMermaid: boolean; source: string } => { - if (!React.isValidElement(children)) return { isMermaid: false, source: '' }; - const props = children.props as Record | undefined; - const className = typeof props?.className === 'string' ? props.className : ''; - if (!className.includes('language-mermaid')) return { isMermaid: false, source: '' }; - // Extract raw mermaid source from the code element's children - const codeChildren = props?.children; - let source = ''; - if (typeof codeChildren === 'string') { - source = codeChildren; - } else if (React.isValidElement(codeChildren)) { - const innerProps = codeChildren.props as Record | undefined; - if (typeof innerProps?.children === 'string') source = innerProps.children; - } - return { isMermaid: true, source }; -}; - const MermaidBlock: React.FC<{ source: string; mode: 'svg' | 'ascii' }> = ({ source, mode }) => { const currentTheme = useCurrentMermaidTheme(); const { isMobile } = useDeviceInfo(); @@ -537,9 +398,9 @@ const MermaidBlock: React.FC<{ source: string; mode: 'svg' | 'ascii' }> = ({ sou const asciiText = ascii || source; return ( -
-
-
{asciiText}
+
+
+
{asciiText}
= ({ sou if (!svg) { return ( -
-
-
{source}
+
+
+
{source}
= ({ sou } return ( -
-
-
+
+
+
= ({ sou ); }; -const CodeBlockWrapper: React.FC = ({ children, className, style, ...props }) => { - const mermaidInfo = getMermaidInfo(children); - const mermaidRenderingMode = useUIStore((state) => state.mermaidRenderingMode); - const codeChild = React.useMemo( - () => ( - React.isValidElement(children) - ? React.cloneElement(children as React.ReactElement>, { 'data-block': true }) - : children - ), - [children], - ); - - const normalizedStyle = React.useMemo(() => { - if (!style) return style; - - const next: React.CSSProperties = { ...style }; - - const normalizeDeclarationString = ( - raw: unknown - ): { value?: string; vars: Record } => { - if (typeof raw !== 'string') return { value: undefined, vars: {} }; - - const [valuePart, ...rest] = raw.split(';').map((p) => p.trim()).filter(Boolean); - const vars: Record = {}; - for (const decl of rest) { - const idx = decl.indexOf(':'); - if (idx === -1) continue; - const prop = decl.slice(0, idx).trim(); - const value = decl.slice(idx + 1).trim(); - if (!prop.startsWith('--') || value.length === 0) continue; - vars[prop] = value; - } - return { value: valuePart, vars }; - }; - - const bg = normalizeDeclarationString((style as React.CSSProperties).backgroundColor); - if (bg.value) { - next.backgroundColor = bg.value; - } - for (const [k, v] of Object.entries(bg.vars)) { - (next as Record)[k] = v; - } - - const fg = normalizeDeclarationString((style as React.CSSProperties).color); - if (fg.value) { - next.color = fg.value; - } - for (const [k, v] of Object.entries(fg.vars)) { - (next as Record)[k] = v; - } - - return next; - }, [style]); - - if (mermaidInfo.isMermaid) { - return ; - } - - return ( -
-      {codeChild}
-    
- ); -}; - -const streamdownComponents = { - pre: CodeBlockWrapper, - table: TableWrapper, -}; - -const streamdownControls = { - code: true, - table: false, -}; - type MermaidControlOptions = { download: boolean; copy: boolean; @@ -727,6 +509,293 @@ const stripLeadingFrontmatter = (markdown: string): string => { export type MarkdownVariant = 'assistant' | 'tool' | 'reasoning'; +type MarkdownStreamBlock = { + key: string; + raw: string; + src: string; + mode: 'full' | 'live'; +}; + +const hasReferenceDefinitions = (text: string): boolean => { + return /^\[[^\]]+\]:\s+\S+/m.test(text) || /^\[\^[^\]]+\]:\s+/m.test(text); +}; + +const hasOpenFence = (raw: string): boolean => { + const match = raw.match(/^[ \t]{0,3}(`{3,}|~{3,})/); + if (!match) return false; + const marker = match[1]; + if (!marker) return false; + const char = marker[0]; + const size = marker.length; + const last = raw.trimEnd().split('\n').at(-1)?.trim() ?? ''; + return !new RegExp(`^[\\t ]{0,3}${char}{${size},}[\\t ]*$`).test(last); +}; + +const healMarkdown = (text: string): string => { + return remend(text, { linkMode: 'text-only' }); +}; + +const fnv1a32 = (input: string): string => { + let hash = 0x811c9dc5; + for (let i = 0; i < input.length; i += 1) { + hash ^= input.charCodeAt(i); + hash = (hash + ((hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24))) >>> 0; + } + return hash.toString(16); +}; + +const buildMarkdownCacheKey = (baseKey: string, raw: string, index: number, mode: 'full' | 'live'): string => { + const sample = raw.length > 400 ? `${raw.slice(0, 200)}${raw.slice(-200)}` : raw; + return `${baseKey}:${index}:${mode}:${raw.length}:${fnv1a32(sample)}`; +}; + +const streamMarkdownBlocks = (text: string, live: boolean, baseKey: string): MarkdownStreamBlock[] => { + if (!live) { + return [{ + key: buildMarkdownCacheKey(baseKey, text, 0, 'full'), + raw: text, + src: text, + mode: 'full', + }]; + } + + const healed = healMarkdown(text); + if (hasReferenceDefinitions(text)) { + return [{ + key: buildMarkdownCacheKey(baseKey, text, 0, 'live'), + raw: text, + src: healed, + mode: 'live', + }]; + } + + const tokens = marked.lexer(text); + const blocks: MarkdownStreamBlock[] = []; + let blockIndex = 0; + for (let index = 0; index < tokens.length; index += 1) { + const token = tokens[index] as Tokens.Generic; + if (token.type === 'space') { + continue; + } + + const raw = token.raw ?? ''; + const isLast = index === tokens.length - 1 || tokens.slice(index + 1).every((nextToken) => nextToken.type === 'space'); + const mode: 'full' | 'live' = isLast ? 'live' : 'full'; + const src = isLast && token.type === 'code' && hasOpenFence(raw) + ? raw + : healMarkdown(raw); + + blocks.push({ + key: buildMarkdownCacheKey(baseKey, raw, blockIndex, mode), + raw, + src, + mode, + }); + blockIndex += 1; + } + + if (blocks.length === 0) { + return [{ + key: buildMarkdownCacheKey(baseKey, text, 0, 'live'), + raw: text, + src: healed, + mode: 'live', + }]; + } + + return blocks; +}; + +const useStableMarkdownBlocks = (text: string, live: boolean, baseKey: string): MarkdownStreamBlock[] => { + const previousRef = React.useRef([]); + + return React.useMemo(() => { + const nextBlocks = streamMarkdownBlocks(text, live, baseKey); + const previousBlocks = previousRef.current; + const stabilized = nextBlocks.map((block, index) => { + const previous = previousBlocks[index]; + if (previous && previous.key === block.key && previous.src === block.src) { + return previous; + } + return block; + }); + + const unchanged = stabilized.length === previousBlocks.length + && stabilized.every((block, index) => block === previousBlocks[index]); + + if (unchanged) { + return previousBlocks; + } + + previousRef.current = stabilized; + return stabilized; + }, [baseKey, live, text]); +}; + +const extractCodeText = (children: React.ReactNode): string => { + if (typeof children === 'string') return children; + if (Array.isArray(children)) { + return children.map((child) => extractCodeText(child)).join(''); + } + if (React.isValidElement(children)) { + return extractCodeText((children.props as { children?: React.ReactNode }).children); + } + return ''; +}; + +const getCodeLanguage = (className: string | undefined): string => { + const match = className?.match(/language-([\w-]+)/); + return match?.[1]?.toLowerCase() ?? 'text'; +}; + +const MarkdownCodeBlock: React.FC<{ + code: string; + language: string; + syntaxTheme: { [key: string]: React.CSSProperties }; +}> = ({ code, language, syntaxTheme }) => { + const [copied, setCopied] = React.useState(false); + + const handleCopy = React.useCallback(async () => { + const result = await copyTextToClipboard(code); + if (!result.ok) return; + setCopied(true); + window.setTimeout(() => setCopied(false), 2000); + }, [code]); + + return ( +
+
+ {language} +
+ +
+
+
+ + {code} + +
+
+ ); +}; + +const buildMarkdownComponents = ({ + syntaxTheme, +}: { + syntaxTheme: { [key: string]: React.CSSProperties }; +}): Components => ({ + table({ children, ...props }) { + return {children}; + }, + h1({ children, ...props }) { + return

{children}

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

{children}

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

{children}

; + }, + h4({ children, ...props }) { + return

{children}

; + }, + h5({ children, ...props }) { + return
{children}
; + }, + h6({ children, ...props }) { + return
{children}
; + }, + p({ children, ...props }) { + return

{children}

; + }, + thead({ children, ...props }) { + return {children}; + }, + tbody({ children, ...props }) { + return {children}; + }, + tr({ children, ...props }) { + return {children}; + }, + th({ children, ...props }) { + return {children}; + }, + td({ children, ...props }) { + return {children}; + }, + ul({ children, ...props }) { + return
    {children}
; + }, + ol({ children, ...props }) { + return
    {children}
; + }, + li({ children, ...props }) { + return
  • {children}
  • ; + }, + blockquote({ children, ...props }) { + return
    {children}
    ; + }, + pre({ children, ...props }) { + const child = React.Children.only(children) as React.ReactElement<{ className?: string; children?: React.ReactNode }>; + const className = child.props.className; + const code = extractCodeText(child.props.children).replace(/\n$/, ''); + const language = getCodeLanguage(className); + if (language === 'mermaid') { + return ; + } + return ; + }, + code({ className, children, ...props }) { + return ( + + {children} + + ); + }, + a({ href, children, ...props }) { + return ( + + {children} + + ); + }, +}); + +const MarkdownBlockView: React.FC<{ + block: MarkdownStreamBlock; + components: Components; +}> = React.memo(({ block, components }) => { + return ( + + {block.src} + + ); +}, (prev, next) => prev.block === next.block && prev.components === next.components); + +MarkdownBlockView.displayName = 'MarkdownBlockView'; + interface MarkdownRendererProps { content: string; part?: Part; @@ -740,7 +809,7 @@ interface MarkdownRendererProps { onShowPopup?: (content: ToolPopupContent) => void; } -const MERMAID_BLOCK_SELECTOR = '[data-streamdown="mermaid-block"]'; +const MERMAID_BLOCK_SELECTOR = '[data-markdown="mermaid-block"]'; const FILE_LINK_SELECTOR = '[data-openchamber-file-link="true"]'; type ParsedFileReference = { @@ -1039,7 +1108,7 @@ const useFileReferenceInteractions = ({ } const annotateFileLinks = () => { - const candidates = container.querySelectorAll('[data-streamdown="inline-code"], a'); + const candidates = container.querySelectorAll('[data-markdown="inline-code"], a'); for (const candidate of Array.from(candidates)) { const rawCandidate = extractPathCandidateFromElement(candidate); @@ -1279,49 +1348,37 @@ export const MarkdownRenderer: React.FC = ({ variant = 'assistant', onShowPopup, }) => { + const currentTheme = useCurrentMermaidTheme(); const { editor, runtime } = useRuntimeAPIs(); - const streamdownContainerRef = React.useRef(null); + const containerRef = React.useRef(null); const effectiveDirectory = useEffectiveDirectory() ?? ''; const mermaidBlocks = React.useMemo(() => extractMermaidBlocks(content), [content]); - useMermaidInlineInteractions({ containerRef: streamdownContainerRef, mermaidBlocks, onShowPopup }); + useMermaidInlineInteractions({ containerRef, mermaidBlocks, onShowPopup }); useFileReferenceInteractions({ - containerRef: streamdownContainerRef, + containerRef, effectiveDirectory, editor, preferRuntimeEditor: runtime.isVSCode, }); - useExternalLinkInteractions({ containerRef: streamdownContainerRef }); - - const shikiThemes = useMarkdownShikiThemes(); - const streamdownPlugins = useStreamdownPlugins(shikiThemes); - const currentMermaidTheme = useCurrentMermaidTheme(); + useExternalLinkInteractions({ containerRef }); + const syntaxTheme = React.useMemo(() => generateSyntaxTheme(currentTheme), [currentTheme]); + const markdownComponents = React.useMemo(() => buildMarkdownComponents({ syntaxTheme }), [syntaxTheme]); const componentKey = `markdown-${part?.id ? `part-${part.id}` : `message-${messageId}`}`; + const markdownBlocks = useStableMarkdownBlocks(content, isStreaming && !disableStreamAnimation, componentKey); - const streamdownClassName = variant === 'tool' - ? 'streamdown-content streamdown-tool' + const markdownClassName = variant === 'tool' + ? 'markdown-content markdown-tool' : variant === 'reasoning' - ? 'streamdown-content streamdown-reasoning' - : 'streamdown-content'; - - const streamdownAnimated = undefined; + ? 'markdown-content markdown-reasoning' + : 'markdown-content leading-relaxed'; const markdownContent = ( -
    - - {content} - +
    +
    + {markdownBlocks.map((block) => ( + + ))} +
    ); @@ -1359,48 +1416,40 @@ export const SimpleMarkdownRenderer: React.FC<{ () => (stripFrontmatter ? stripLeadingFrontmatter(content) : content), [content, stripFrontmatter], ); - const streamdownContainerRef = React.useRef(null); + const currentTheme = useCurrentMermaidTheme(); + const containerRef = React.useRef(null); const effectiveDirectory = useEffectiveDirectory() ?? ''; const mermaidBlocks = React.useMemo(() => extractMermaidBlocks(renderedContent), [renderedContent]); useMermaidInlineInteractions({ - containerRef: streamdownContainerRef, + containerRef, mermaidBlocks, onShowPopup, allowWheelZoom: allowMermaidWheelZoom, }); useFileReferenceInteractions({ - containerRef: streamdownContainerRef, + containerRef, effectiveDirectory, editor, preferRuntimeEditor: runtime.isVSCode, }); - useExternalLinkInteractions({ containerRef: streamdownContainerRef, enabled: !disableLinkSafety }); + useExternalLinkInteractions({ containerRef, enabled: !disableLinkSafety }); + const syntaxTheme = React.useMemo(() => generateSyntaxTheme(currentTheme), [currentTheme]); + const markdownComponents = React.useMemo(() => buildMarkdownComponents({ syntaxTheme }), [syntaxTheme]); + const markdownBlocks = useStableMarkdownBlocks(renderedContent, false, `simple:${variant}`); - const shikiThemes = useMarkdownShikiThemes(); - const streamdownPlugins = useStreamdownPlugins(shikiThemes); - const currentMermaidTheme = useCurrentMermaidTheme(); - - const streamdownClassName = variant === 'tool' - ? 'streamdown-content streamdown-tool' + const markdownClassName = variant === 'tool' + ? 'markdown-content markdown-tool' : variant === 'reasoning' - ? 'streamdown-content streamdown-reasoning' - : 'streamdown-content'; + ? 'markdown-content markdown-reasoning' + : 'markdown-content leading-relaxed'; return ( -
    - - {renderedContent} - +
    +
    + {markdownBlocks.map((block) => ( + + ))} +
    ); }; diff --git a/packages/ui/src/components/chat/message/ToolOutputDialog.tsx b/packages/ui/src/components/chat/message/ToolOutputDialog.tsx index 091b4730..cf70a31e 100644 --- a/packages/ui/src/components/chat/message/ToolOutputDialog.tsx +++ b/packages/ui/src/components/chat/message/ToolOutputDialog.tsx @@ -962,7 +962,7 @@ const MermaidPreviewDialog: React.FC<{ content={mermaidMarkdown} variant="tool" allowMermaidWheelZoom - className="streamdown-mermaid-fullscreen h-full [&_[data-streamdown='mermaid-block']_button]:hidden" + className="markdown-mermaid-fullscreen h-full [&_[data-markdown='mermaid-block']_button]:hidden" mermaidControls={{ download: false, copy: false, diff --git a/packages/ui/src/index.css b/packages/ui/src/index.css index 60530bca..668b0419 100644 --- a/packages/ui/src/index.css +++ b/packages/ui/src/index.css @@ -870,61 +870,70 @@ html:not(.dark) .chat-scroll { } /* Text font: IBM Plex Sans */ -.streamdown-content { +.markdown-content { font-family: var(--font-sans); font-size: var(--text-markdown); + line-height: inherit; +} + +.markdown-content p, +.markdown-content li, +.markdown-content blockquote, +.markdown-content td, +.markdown-content th { + line-height: inherit; } /* Tool markdown should render at code size to match tool card typography. */ -.streamdown-content.streamdown-tool { +.markdown-content.markdown-tool { font-size: var(--text-code) !important; } /* Reasoning markdown renders at meta size, dimmed. */ -.streamdown-content.streamdown-reasoning { +.markdown-content.markdown-reasoning { font-size: var(--text-meta); color: var(--muted-foreground); } -.streamdown-content.streamdown-reasoning strong, -.streamdown-content.streamdown-reasoning b, -.streamdown-content.streamdown-reasoning [data-streamdown="strong"], -.streamdown-content.streamdown-reasoning [data-streamdown="bold"] { +.markdown-content.markdown-reasoning strong, +.markdown-content.markdown-reasoning b, +.markdown-content.markdown-reasoning [data-markdown="strong"], +.markdown-content.markdown-reasoning [data-markdown="bold"] { font-weight: inherit !important; color: inherit !important; font-style: inherit !important; } -.streamdown-content.streamdown-tool code, -.streamdown-content.streamdown-tool pre { +.markdown-content.markdown-tool code, +.markdown-content.markdown-tool pre { font-size: inherit !important; } /* Streamdown headers: keep proportional but not oversized */ -.streamdown-content h1 { +.markdown-content h1 { font-size: 1.125em; font-weight: 600; margin-top: 1em; margin-bottom: 0.5em; } -.streamdown-content h2 { +.markdown-content h2 { font-size: 1.0625em; font-weight: 600; margin-top: 0.875em; margin-bottom: 0.375em; } -.streamdown-content h3 { +.markdown-content h3 { font-size: 1em; font-weight: 600; margin-top: 0.75em; margin-bottom: 0.25em; } -.streamdown-content h4, -.streamdown-content h5, -.streamdown-content h6 { +.markdown-content h4, +.markdown-content h5, +.markdown-content h6 { font-size: 1em; font-weight: 600; margin-top: 0.625em; @@ -932,14 +941,14 @@ html:not(.dark) .chat-scroll { } /* Code font: IBM Plex Mono */ -.streamdown-content code, -.streamdown-content pre { +.markdown-content code, +.markdown-content pre { font-family: var(--font-mono); font-size: var(--text-code); } /* Override Streamdown's hardcoded bg-muted for inline code - use theme colors instead */ -.streamdown-content code[data-streamdown="inline-code"] { +.markdown-content code[data-markdown="inline-code"] { background-color: var(--markdown-inline-code-bg, var(--surface-muted)) !important; color: var(--markdown-inline-code, var(--foreground)) !important; word-break: break-all; @@ -947,123 +956,123 @@ html:not(.dark) .chat-scroll { } /* Markdown headings - use theme colors */ -.streamdown-content h1 { +.markdown-content h1 { color: var(--markdown-heading1, var(--primary)); } -.streamdown-content h2 { +.markdown-content h2 { color: var(--markdown-heading2, var(--primary)); } -.streamdown-content h3 { +.markdown-content h3 { color: var(--markdown-heading3, var(--primary)); } -.streamdown-content h4, -.streamdown-content h5, -.streamdown-content h6 { +.markdown-content h4, +.markdown-content h5, +.markdown-content h6 { color: var(--markdown-heading4, var(--foreground)); } /* Markdown links - use theme colors */ -.streamdown-content a { +.markdown-content a { color: var(--markdown-link, var(--primary)); } -.streamdown-content a:hover { +.markdown-content a:hover { color: var(--markdown-link-hover, var(--primary)); } -.streamdown-content [data-openchamber-file-link="true"] { +.markdown-content [data-openchamber-file-link="true"] { color: var(--markdown-link, var(--primary)); cursor: pointer; text-decoration: underline; text-underline-offset: 2px; } -.streamdown-content [data-openchamber-file-link="true"]:hover { +.markdown-content [data-openchamber-file-link="true"]:hover { color: var(--markdown-link-hover, var(--primary)); } /* Markdown blockquote - use theme colors */ -.streamdown-content blockquote { +.markdown-content blockquote { color: var(--markdown-blockquote, var(--muted-foreground)); border-left-color: var(--markdown-blockquote-border, var(--border)); } /* Markdown horizontal rule - use theme colors */ -.streamdown-content hr { +.markdown-content hr { border-color: var(--markdown-hr, var(--border)); } /* Markdown bold/italic/strikethrough - use theme colors */ -.streamdown-content strong { +.markdown-content strong { color: var(--markdown-bold, var(--foreground)); } -.streamdown-content em { +.markdown-content em { color: var(--markdown-italic, var(--foreground)); } -.streamdown-content del { +.markdown-content del { color: var(--markdown-strikethrough, var(--muted-foreground)); } /* Streamdown code blocks: ensure light/dark Shiki vars work even when Tailwind doesn't scan Streamdown's internal class names. */ -.streamdown-content [data-streamdown="code-block-body"] { +.markdown-content [data-markdown="code-block-body"] { font-size: var(--text-code); background-color: transparent; color: var(--shiki-light, inherit); } -.streamdown-content [data-streamdown="code-block-body"] span[style*="--sdm-c"] { +.markdown-content [data-markdown="code-block-body"] span[style*="--sdm-c"] { color: var(--sdm-c) !important; } -.streamdown-content [data-streamdown="code-block-body"] span[style*="--sdm-tbg"] { +.markdown-content [data-markdown="code-block-body"] span[style*="--sdm-tbg"] { background-color: var(--sdm-tbg) !important; } -.streamdown-content [data-streamdown="code-block-body"] span[style*="--shiki-light"] { +.markdown-content [data-markdown="code-block-body"] span[style*="--shiki-light"] { color: var(--shiki-light) !important; } -.streamdown-content [data-streamdown="code-block-body"] span[style*="--shiki-light-bg"] { +.markdown-content [data-markdown="code-block-body"] span[style*="--shiki-light-bg"] { background-color: var(--shiki-light-bg) !important; } -.dark .streamdown-content [data-streamdown="code-block-body"] { +.dark .markdown-content [data-markdown="code-block-body"] { background-color: transparent; color: var(--shiki-dark, var(--shiki-light, inherit)); } -.dark .streamdown-content [data-streamdown="code-block-body"] span[style*="--shiki-dark"] { +.dark .markdown-content [data-markdown="code-block-body"] span[style*="--shiki-dark"] { color: var(--shiki-dark) !important; } -.dark .streamdown-content [data-streamdown="code-block-body"] span[style*="--shiki-dark-bg"] { +.dark .markdown-content [data-markdown="code-block-body"] span[style*="--shiki-dark-bg"] { background-color: var(--shiki-dark-bg) !important; } /* Fix list styling - override Tailwind reset */ -.streamdown-content ul { +.markdown-content ul { list-style-type: disc; list-style-position: outside; padding-left: 1.5em; } -.streamdown-content ol { +.markdown-content ol { list-style-type: decimal; list-style-position: outside; padding-left: 1.5em; } -.streamdown-content li { +.markdown-content li { display: list-item; } /* Reduce code block header height */ -[data-streamdown="code-block-header"] { +[data-markdown="code-block-header"] { margin: 0; padding: 0.375rem 0.75rem; display: flex; @@ -1078,7 +1087,7 @@ html:not(.dark) .chat-scroll { } /* Disable Streamdown's code block virtualization and keep block chrome fixed */ -.streamdown-content [data-streamdown="code-block"] { +.markdown-content [data-markdown="code-block"] { content-visibility: visible !important; contain-intrinsic-size: none !important; position: relative; @@ -1095,7 +1104,7 @@ html:not(.dark) .chat-scroll { border-radius: 0.75rem; } -.streamdown-content [data-streamdown="code-block"] > div:not([data-streamdown]) { +.markdown-content [data-markdown="code-block"] > div:not([data-markdown]) { position: absolute; top: 0; right: 0; @@ -1106,7 +1115,7 @@ html:not(.dark) .chat-scroll { } /* Streamdown code block body: scroll text content within the card body */ -.streamdown-content [data-streamdown="code-block-body"] { +.markdown-content [data-markdown="code-block-body"] { display: block; inline-size: 100%; min-width: 0; @@ -1122,7 +1131,7 @@ html:not(.dark) .chat-scroll { border-radius: 0; } -.streamdown-content [data-streamdown="code-block-body"] pre { +.markdown-content [data-markdown="code-block-body"] pre { display: block; width: 100%; min-width: 100%; @@ -1135,21 +1144,21 @@ html:not(.dark) .chat-scroll { background: transparent !important; } -.streamdown-content [data-streamdown="code-block-body"] > * { +.markdown-content [data-markdown="code-block-body"] > * { border: 0 !important; border-radius: 0 !important; box-shadow: none !important; background: transparent !important; } -.streamdown-content [data-streamdown="code-block-body"] code { +.markdown-content [data-markdown="code-block-body"] code { white-space: pre; overflow-wrap: normal; word-break: normal; background: transparent !important; } -.streamdown-content [data-streamdown="code-block-actions"] { +.markdown-content [data-markdown="code-block-actions"] { pointer-events: auto; transition: opacity 0.15s ease; border: 0 !important; @@ -1158,7 +1167,7 @@ html:not(.dark) .chat-scroll { gap: 0.125rem; } -.streamdown-content [data-streamdown="code-block-actions"] button { +.markdown-content [data-markdown="code-block-actions"] button { border: 0 !important; background: transparent !important; box-shadow: none !important; @@ -1167,26 +1176,26 @@ html:not(.dark) .chat-scroll { min-height: 1.5rem; } -.streamdown-content [data-streamdown="code-block-actions"] button svg { +.markdown-content [data-markdown="code-block-actions"] button svg { width: 0.875rem; height: 0.875rem; } @media (hover: hover) and (pointer: fine) { - .streamdown-content [data-streamdown="code-block"] [data-streamdown="code-block-actions"] { + .markdown-content [data-markdown="code-block"] [data-markdown="code-block-actions"] { opacity: 0; pointer-events: none; } - .streamdown-content [data-streamdown="code-block"]:hover [data-streamdown="code-block-actions"], - .streamdown-content [data-streamdown="code-block"]:focus-within [data-streamdown="code-block-actions"] { + .markdown-content [data-markdown="code-block"]:hover [data-markdown="code-block-actions"], + .markdown-content [data-markdown="code-block"]:focus-within [data-markdown="code-block-actions"] { opacity: 1; pointer-events: auto; } } /* Mermaid block: position context for controls wrapper. */ -[data-streamdown="mermaid-block"] { +[data-markdown="mermaid-block"] { position: relative; border-radius: 0.75rem; border: 1px solid var(--interactive-border); @@ -1194,17 +1203,17 @@ html:not(.dark) .chat-scroll { padding: 0.5rem; } -[data-streamdown="mermaid-block"] [data-streamdown="mermaid-scroll"] { +[data-markdown="mermaid-block"] [data-markdown="mermaid-scroll"] { overflow: auto; } -[data-streamdown="mermaid-block"] [data-streamdown="mermaid"] { +[data-markdown="mermaid-block"] [data-markdown="mermaid"] { width: max-content; min-width: 100%; display: block; } -[data-streamdown="mermaid-block"] [data-streamdown="mermaid-ascii"] { +[data-markdown="mermaid-block"] [data-markdown="mermaid-ascii"] { margin: 0; white-space: pre; width: max-content; @@ -1214,30 +1223,30 @@ html:not(.dark) .chat-scroll { background: transparent; } -[data-streamdown="mermaid-block"] [data-streamdown="mermaid"] svg { +[data-markdown="mermaid-block"] [data-markdown="mermaid"] svg { width: auto !important; min-width: 100%; max-width: none !important; height: auto !important; } -[data-streamdown="mermaid-block"] svg { +[data-markdown="mermaid-block"] svg { display: block; background: var(--surface-elevated); } /* Fullscreen Mermaid popup: scale diagrams to fill available viewport panel area. */ -.streamdown-mermaid-fullscreen { +.markdown-mermaid-fullscreen { height: 100%; width: 100%; } -.streamdown-mermaid-fullscreen .streamdown-content { +.markdown-mermaid-fullscreen .markdown-content { height: 100%; width: 100%; } -.streamdown-mermaid-fullscreen [data-streamdown="mermaid-block"] { +.markdown-mermaid-fullscreen [data-markdown="mermaid-block"] { width: 100%; height: 100%; min-height: 100%; @@ -1247,20 +1256,20 @@ html:not(.dark) .chat-scroll { background: var(--surface-elevated); } -.streamdown-mermaid-fullscreen [data-streamdown="mermaid-block"] [data-streamdown="mermaid-scroll"] { +.markdown-mermaid-fullscreen [data-markdown="mermaid-block"] [data-markdown="mermaid-scroll"] { width: 100%; height: 100%; overflow: auto; } -.streamdown-mermaid-fullscreen [data-streamdown="mermaid-block"] [data-streamdown="mermaid"] { +.markdown-mermaid-fullscreen [data-markdown="mermaid-block"] [data-markdown="mermaid"] { width: max-content; min-width: 100%; min-height: 100%; display: block; } -.streamdown-mermaid-fullscreen [data-streamdown="mermaid-block"] [data-streamdown="mermaid-ascii"] { +.markdown-mermaid-fullscreen [data-markdown="mermaid-block"] [data-markdown="mermaid-ascii"] { margin: 0; white-space: pre; width: max-content; @@ -1271,7 +1280,7 @@ html:not(.dark) .chat-scroll { background: transparent; } -.streamdown-mermaid-fullscreen [data-streamdown="mermaid-block"] svg { +.markdown-mermaid-fullscreen [data-markdown="mermaid-block"] svg { width: auto !important; height: auto !important; max-width: none !important; diff --git a/packages/ui/src/lib/shiki/appThemeRegistry.ts b/packages/ui/src/lib/shiki/appThemeRegistry.ts index 7043f8ff..5b54c434 100644 --- a/packages/ui/src/lib/shiki/appThemeRegistry.ts +++ b/packages/ui/src/lib/shiki/appThemeRegistry.ts @@ -89,7 +89,3 @@ export const ensurePierreThemeRegistered = (theme: Theme): void => { registerCustomTheme(id, async () => resolved); registeredPierreThemes.add(id); }; - -export const getStreamdownThemePair = (light: Theme, dark: Theme) => { - return [getResolvedShikiTheme(light), getResolvedShikiTheme(dark)] as const; -}; diff --git a/packages/ui/src/styles/mobile.css b/packages/ui/src/styles/mobile.css index ec18409b..cc0de342 100644 --- a/packages/ui/src/styles/mobile.css +++ b/packages/ui/src/styles/mobile.css @@ -150,8 +150,8 @@ -webkit-overflow-scrolling: touch; } - /* Keep Streamdown code blocks horizontally scrollable on mobile. */ - :root.mobile-pointer:not(.desktop-runtime) .streamdown-content [data-streamdown="code-block-body"] { + /* Keep markdown code blocks horizontally scrollable on mobile. */ + :root.mobile-pointer:not(.desktop-runtime) .markdown-content [data-markdown="code-block-body"] { overflow-x: auto !important; overflow-y: hidden !important; -webkit-overflow-scrolling: touch; diff --git a/packages/ui/src/types/streamdown.d.ts b/packages/ui/src/types/streamdown.d.ts deleted file mode 100644 index 78644b41..00000000 --- a/packages/ui/src/types/streamdown.d.ts +++ /dev/null @@ -1,36 +0,0 @@ -declare module 'streamdown' { - import type { FC, ReactNode, ComponentType } from 'react'; - - export interface StreamdownProps { - children: string; - mode?: 'streaming' | 'static'; - isAnimating?: boolean; - animated?: { - animation?: string; - duration?: number; - easing?: string; - sep?: 'word' | 'char'; - }; - className?: string; - - shikiTheme?: readonly [string | object, string | object]; - - controls?: boolean | { - code?: boolean; - table?: boolean; - mermaid?: boolean | { - download?: boolean; - copy?: boolean; - fullscreen?: boolean; - panZoom?: boolean; - }; - }; - plugins?: Record; - mermaid?: Record; - components?: { - [key: string]: ComponentType<{ children?: ReactNode; className?: string; [key: string]: unknown }>; - }; - } - - export const Streamdown: FC; -}