refactor: replace chat markdown renderer
- Replace Streamdown-based chat markdown rendering with the new renderer path - Preserve OpenChamber table, code block, and mermaid presentation - Remove leftover Streamdown dependencies and compatibility hooks
This commit is contained in:
@@ -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=="],
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 = <T extends object>(value: T, id: string): T => {
|
||||
const existingPrimitive = (value as Record<symbol, unknown>)[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<string, unknown>) },
|
||||
`vscode-shiki-light:${String((provided.light as { name?: unknown })?.name ?? 'theme')}`,
|
||||
);
|
||||
const dark = withStableStringId(
|
||||
{ ...(provided.dark as Record<string, unknown>) },
|
||||
`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<Parameters<typeof createCodePlugin>[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<HTMLDivElement>(null);
|
||||
|
||||
return (
|
||||
<div className="group my-4 flex flex-col space-y-2" data-streamdown="table-wrapper" ref={tableRef}>
|
||||
<div className="group my-4 flex flex-col space-y-2" data-markdown="table-wrapper" ref={tableRef}>
|
||||
<div className="flex items-center justify-end gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<TableCopyButton tableRef={tableRef} />
|
||||
<TableDownloadButton tableRef={tableRef} />
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className={cn('w-full border-collapse border border-border', className)} data-streamdown="table">
|
||||
<div className="overflow-x-auto rounded-lg border border-border/80 bg-[var(--surface-elevated)]">
|
||||
<table className={cn('w-full border-collapse text-sm', className)} data-markdown="table">
|
||||
{children}
|
||||
</table>
|
||||
</div>
|
||||
@@ -439,27 +321,6 @@ const TableWrapper: React.FC<{ children?: React.ReactNode; className?: string }>
|
||||
);
|
||||
};
|
||||
|
||||
type CodeBlockWrapperProps = React.HTMLAttributes<HTMLPreElement> & {
|
||||
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<string, unknown> | 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<string, unknown> | 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 (
|
||||
<div data-streamdown="mermaid-block" className="group">
|
||||
<div data-streamdown="mermaid-scroll">
|
||||
<pre data-streamdown="mermaid-ascii">{asciiText}</pre>
|
||||
<div data-markdown="mermaid-block" className="group">
|
||||
<div data-markdown="mermaid-scroll">
|
||||
<pre data-markdown="mermaid-ascii">{asciiText}</pre>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
@@ -561,9 +422,9 @@ const MermaidBlock: React.FC<{ source: string; mode: 'svg' | 'ascii' }> = ({ sou
|
||||
|
||||
if (!svg) {
|
||||
return (
|
||||
<div data-streamdown="mermaid-block" className="group">
|
||||
<div data-streamdown="mermaid-scroll">
|
||||
<pre data-streamdown="mermaid-ascii">{source}</pre>
|
||||
<div data-markdown="mermaid-block" className="group">
|
||||
<div data-markdown="mermaid-scroll">
|
||||
<pre data-markdown="mermaid-ascii">{source}</pre>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
@@ -584,9 +445,9 @@ const MermaidBlock: React.FC<{ source: string; mode: 'svg' | 'ascii' }> = ({ sou
|
||||
}
|
||||
|
||||
return (
|
||||
<div data-streamdown="mermaid-block" className="group">
|
||||
<div data-streamdown="mermaid-scroll">
|
||||
<div data-streamdown="mermaid" dangerouslySetInnerHTML={{ __html: svg }} />
|
||||
<div data-markdown="mermaid-block" className="group">
|
||||
<div data-markdown="mermaid-scroll">
|
||||
<div data-markdown="mermaid" dangerouslySetInnerHTML={{ __html: svg }} />
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
@@ -613,85 +474,6 @@ const MermaidBlock: React.FC<{ source: string; mode: 'svg' | 'ascii' }> = ({ sou
|
||||
);
|
||||
};
|
||||
|
||||
const CodeBlockWrapper: React.FC<CodeBlockWrapperProps> = ({ 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<Record<string, unknown>>, { 'data-block': true })
|
||||
: children
|
||||
),
|
||||
[children],
|
||||
);
|
||||
|
||||
const normalizedStyle = React.useMemo<React.CSSProperties | undefined>(() => {
|
||||
if (!style) return style;
|
||||
|
||||
const next: React.CSSProperties = { ...style };
|
||||
|
||||
const normalizeDeclarationString = (
|
||||
raw: unknown
|
||||
): { value?: string; vars: Record<string, string> } => {
|
||||
if (typeof raw !== 'string') return { value: undefined, vars: {} };
|
||||
|
||||
const [valuePart, ...rest] = raw.split(';').map((p) => p.trim()).filter(Boolean);
|
||||
const vars: Record<string, string> = {};
|
||||
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<string, string>)[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<string, string>)[k] = v;
|
||||
}
|
||||
|
||||
return next;
|
||||
}, [style]);
|
||||
|
||||
if (mermaidInfo.isMermaid) {
|
||||
return <MermaidBlock source={mermaidInfo.source} mode={mermaidRenderingMode} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<pre
|
||||
{...props}
|
||||
className={cn(className, 'w-full min-w-full')}
|
||||
style={normalizedStyle}
|
||||
>
|
||||
{codeChild}
|
||||
</pre>
|
||||
);
|
||||
};
|
||||
|
||||
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<MarkdownStreamBlock[]>([]);
|
||||
|
||||
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 (
|
||||
<div data-component="markdown-code" className="my-4 group overflow-hidden rounded-2xl border border-border/80 bg-[var(--surface-elevated)]">
|
||||
<div className="flex items-center justify-between border-b border-border/70 px-3 py-1.5">
|
||||
<span className="font-mono text-[13px] text-muted-foreground">{language}</span>
|
||||
<div className="opacity-0 transition-opacity group-hover:opacity-100">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { void handleCopy(); }}
|
||||
className="p-1 rounded hover:bg-interactive-hover/60 text-muted-foreground hover:text-foreground transition-colors"
|
||||
title={copied ? 'Copied' : 'Copy code'}
|
||||
>
|
||||
{copied ? <RiCheckLine className="size-3.5" /> : <RiFileCopyLine className="size-3.5" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-3 py-2.5">
|
||||
<SyntaxHighlighter
|
||||
language={language}
|
||||
style={syntaxTheme}
|
||||
customStyle={{ margin: 0, background: 'transparent', padding: 0, fontSize: 'var(--text-code)', lineHeight: 'var(--markdown-code-block-line-height)' }}
|
||||
codeTagProps={{ style: { fontSize: 'var(--text-code)', lineHeight: 'var(--markdown-code-block-line-height)' } }}
|
||||
PreTag="pre"
|
||||
>
|
||||
{code}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const buildMarkdownComponents = ({
|
||||
syntaxTheme,
|
||||
}: {
|
||||
syntaxTheme: { [key: string]: React.CSSProperties };
|
||||
}): Components => ({
|
||||
table({ children, ...props }) {
|
||||
return <TableWrapper className={props.className}>{children}</TableWrapper>;
|
||||
},
|
||||
h1({ children, ...props }) {
|
||||
return <h1 {...props} className={cn('typography-markdown-h1 mt-4 mb-2 text-[var(--markdown-heading1,var(--primary))] font-semibold', props.className)}>{children}</h1>;
|
||||
},
|
||||
h2({ children, ...props }) {
|
||||
return <h2 {...props} className={cn('typography-markdown-h2 mt-3.5 mb-1.5 text-[var(--markdown-heading2,var(--primary))] font-semibold', props.className)}>{children}</h2>;
|
||||
},
|
||||
h3({ children, ...props }) {
|
||||
return <h3 {...props} className={cn('typography-markdown-h3 mt-3 mb-1 text-[var(--markdown-heading3,var(--primary))] font-semibold', props.className)}>{children}</h3>;
|
||||
},
|
||||
h4({ children, ...props }) {
|
||||
return <h4 {...props} className={cn('typography-markdown-h4 mt-2.5 mb-1 text-[var(--markdown-heading4,var(--foreground))] font-semibold', props.className)}>{children}</h4>;
|
||||
},
|
||||
h5({ children, ...props }) {
|
||||
return <h5 {...props} className={cn('typography-markdown-h4 mt-2.5 mb-1 text-[var(--markdown-heading4,var(--foreground))] font-semibold', props.className)}>{children}</h5>;
|
||||
},
|
||||
h6({ children, ...props }) {
|
||||
return <h6 {...props} className={cn('typography-markdown-h4 mt-2.5 mb-1 text-[var(--markdown-heading4,var(--foreground))] font-semibold', props.className)}>{children}</h6>;
|
||||
},
|
||||
p({ children, ...props }) {
|
||||
return <p {...props} className={cn('typography-markdown-body my-2 text-foreground/90', props.className)}>{children}</p>;
|
||||
},
|
||||
thead({ children, ...props }) {
|
||||
return <thead {...props} className={cn('[&_tr]:border-b [&_tr]:border-border/80', props.className)}>{children}</thead>;
|
||||
},
|
||||
tbody({ children, ...props }) {
|
||||
return <tbody {...props} className={cn('[&_tr:last-child]:border-0', props.className)}>{children}</tbody>;
|
||||
},
|
||||
tr({ children, ...props }) {
|
||||
return <tr {...props} className={cn('border-b border-border/60', props.className)}>{children}</tr>;
|
||||
},
|
||||
th({ children, ...props }) {
|
||||
return <th {...props} className={cn('border-r border-border/60 px-4 py-2.5 text-left align-middle font-semibold text-foreground last:border-r-0', props.className)}>{children}</th>;
|
||||
},
|
||||
td({ children, ...props }) {
|
||||
return <td {...props} className={cn('border-r border-border/60 px-4 py-2.5 align-middle text-foreground/90 last:border-r-0', props.className)}>{children}</td>;
|
||||
},
|
||||
ul({ children, ...props }) {
|
||||
return <ul {...props} className={cn('typography-markdown-body my-2 pl-6', props.className)}>{children}</ul>;
|
||||
},
|
||||
ol({ children, ...props }) {
|
||||
return <ol {...props} className={cn('typography-markdown-body my-2 pl-6', props.className)}>{children}</ol>;
|
||||
},
|
||||
li({ children, ...props }) {
|
||||
return <li {...props} className={cn('typography-markdown-body my-0.5 text-foreground/90', props.className)}>{children}</li>;
|
||||
},
|
||||
blockquote({ children, ...props }) {
|
||||
return <blockquote {...props} className={cn('my-3 border-l-2 border-[var(--markdown-blockquote-border,var(--border))] pl-4 typography-markdown-body text-[var(--markdown-blockquote,var(--muted-foreground))]', props.className)}>{children}</blockquote>;
|
||||
},
|
||||
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 <MermaidBlock source={code} mode={useUIStore.getState().mermaidRenderingMode} />;
|
||||
}
|
||||
return <MarkdownCodeBlock code={code} language={language} syntaxTheme={syntaxTheme} {...props} />;
|
||||
},
|
||||
code({ className, children, ...props }) {
|
||||
return (
|
||||
<code
|
||||
{...props}
|
||||
className={cn('rounded bg-[var(--surface-elevated)] px-1 py-0.5 font-mono text-[0.95em]', className)}
|
||||
data-markdown="inline-code"
|
||||
>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
},
|
||||
a({ href, children, ...props }) {
|
||||
return (
|
||||
<a
|
||||
{...props}
|
||||
href={href}
|
||||
target={isExternalHttpUrl(href ?? '') ? '_blank' : undefined}
|
||||
rel={isExternalHttpUrl(href ?? '') ? 'noopener noreferrer' : undefined}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const MarkdownBlockView: React.FC<{
|
||||
block: MarkdownStreamBlock;
|
||||
components: Components;
|
||||
}> = React.memo(({ block, components }) => {
|
||||
return (
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]} components={components}>
|
||||
{block.src}
|
||||
</ReactMarkdown>
|
||||
);
|
||||
}, (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<HTMLElement>('[data-streamdown="inline-code"], a');
|
||||
const candidates = container.querySelectorAll<HTMLElement>('[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<MarkdownRendererProps> = ({
|
||||
variant = 'assistant',
|
||||
onShowPopup,
|
||||
}) => {
|
||||
const currentTheme = useCurrentMermaidTheme();
|
||||
const { editor, runtime } = useRuntimeAPIs();
|
||||
const streamdownContainerRef = React.useRef<HTMLDivElement>(null);
|
||||
const containerRef = React.useRef<HTMLDivElement>(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 = (
|
||||
<div className={cn('break-words w-full min-w-0', className)} ref={streamdownContainerRef}>
|
||||
<Streamdown
|
||||
key={`streamdown-${componentKey}-${currentMermaidTheme.metadata.id}:${currentMermaidTheme.metadata.variant}`}
|
||||
mode={isStreaming && !disableStreamAnimation ? 'streaming' : 'static'}
|
||||
shikiTheme={shikiThemes}
|
||||
className={streamdownClassName}
|
||||
controls={streamdownControls}
|
||||
plugins={streamdownPlugins}
|
||||
components={streamdownComponents}
|
||||
animated={disableStreamAnimation ? undefined : streamdownAnimated}
|
||||
isAnimating={disableStreamAnimation ? false : isStreaming}
|
||||
// @ts-expect-error Streamdown type missing linkSafety in older minor
|
||||
linkSafety={{ enabled: false }}
|
||||
>
|
||||
{content}
|
||||
</Streamdown>
|
||||
<div className={cn('break-words w-full min-w-0', className)} ref={containerRef}>
|
||||
<div className={markdownClassName}>
|
||||
{markdownBlocks.map((block) => (
|
||||
<MarkdownBlockView key={block.key} block={block} components={markdownComponents} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1359,48 +1416,40 @@ export const SimpleMarkdownRenderer: React.FC<{
|
||||
() => (stripFrontmatter ? stripLeadingFrontmatter(content) : content),
|
||||
[content, stripFrontmatter],
|
||||
);
|
||||
const streamdownContainerRef = React.useRef<HTMLDivElement>(null);
|
||||
const currentTheme = useCurrentMermaidTheme();
|
||||
const containerRef = React.useRef<HTMLDivElement>(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 (
|
||||
<div className={cn('break-words w-full min-w-0', className)} ref={streamdownContainerRef}>
|
||||
<Streamdown
|
||||
key={`streamdown-simple-${currentMermaidTheme.metadata.id}:${currentMermaidTheme.metadata.variant}`}
|
||||
mode="static"
|
||||
shikiTheme={shikiThemes}
|
||||
className={streamdownClassName}
|
||||
controls={streamdownControls}
|
||||
plugins={streamdownPlugins}
|
||||
components={streamdownComponents}
|
||||
// @ts-expect-error Streamdown type missing linkSafety in older minor
|
||||
linkSafety={{ enabled: false }}
|
||||
>
|
||||
{renderedContent}
|
||||
</Streamdown>
|
||||
<div className={cn('break-words w-full min-w-0', className)} ref={containerRef}>
|
||||
<div className={markdownClassName}>
|
||||
{markdownBlocks.map((block) => (
|
||||
<MarkdownBlockView key={block.key} block={block} components={markdownComponents} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
+79
-70
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
-36
@@ -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<string, unknown>;
|
||||
mermaid?: Record<string, unknown>;
|
||||
components?: {
|
||||
[key: string]: ComponentType<{ children?: ReactNode; className?: string; [key: string]: unknown }>;
|
||||
};
|
||||
}
|
||||
|
||||
export const Streamdown: FC<StreamdownProps>;
|
||||
}
|
||||
Reference in New Issue
Block a user