mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
Add streaming markdown renderer
This commit is contained in:
parent
07f910dce3
commit
7fff6d1cce
14 changed files with 739 additions and 60 deletions
|
|
@ -195,6 +195,7 @@ const pureChatStateRestrictions = [
|
|||
];
|
||||
const chatExternalDomBridgeFiles = [
|
||||
"src/features/chat/ui/message-stream/markdown-renderer.ts",
|
||||
"src/features/chat/ui/message-stream/stream-markdown-renderer.ts",
|
||||
"src/features/chat/ui/message-stream/virtualizer.ts",
|
||||
];
|
||||
const chatPreactDomBridgeFiles = [
|
||||
|
|
|
|||
483
package-lock.json
generated
483
package-lock.json
generated
|
|
@ -12,6 +12,7 @@
|
|||
"@preact/signals": "^2.9.1",
|
||||
"@tanstack/query-core": "^5.101.0",
|
||||
"@tanstack/virtual-core": "3.17.0",
|
||||
"micromark": "^4.0.2",
|
||||
"preact": "^10.29.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
@ -2627,6 +2628,15 @@
|
|||
"@types/tern": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/debug": {
|
||||
"version": "4.1.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz",
|
||||
"integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/ms": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/deep-eql": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
|
||||
|
|
@ -2673,6 +2683,12 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/ms": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
|
||||
"integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "25.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz",
|
||||
|
|
@ -3546,6 +3562,16 @@
|
|||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/character-entities": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz",
|
||||
"integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
|
|
@ -3746,7 +3772,6 @@
|
|||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
|
|
@ -3767,6 +3792,19 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/decode-named-character-reference": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz",
|
||||
"integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"character-entities": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/deep-is": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
|
||||
|
|
@ -3810,6 +3848,15 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/dequal": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
||||
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
|
|
@ -3820,6 +3867,19 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/devlop": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
|
||||
"integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dequal": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/doctrine": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
|
||||
|
|
@ -7015,6 +7075,426 @@
|
|||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz",
|
||||
"integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "GitHub Sponsors",
|
||||
"url": "https://github.com/sponsors/unifiedjs"
|
||||
},
|
||||
{
|
||||
"type": "OpenCollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/debug": "^4.0.0",
|
||||
"debug": "^4.0.0",
|
||||
"decode-named-character-reference": "^1.0.0",
|
||||
"devlop": "^1.0.0",
|
||||
"micromark-core-commonmark": "^2.0.0",
|
||||
"micromark-factory-space": "^2.0.0",
|
||||
"micromark-util-character": "^2.0.0",
|
||||
"micromark-util-chunked": "^2.0.0",
|
||||
"micromark-util-combine-extensions": "^2.0.0",
|
||||
"micromark-util-decode-numeric-character-reference": "^2.0.0",
|
||||
"micromark-util-encode": "^2.0.0",
|
||||
"micromark-util-normalize-identifier": "^2.0.0",
|
||||
"micromark-util-resolve-all": "^2.0.0",
|
||||
"micromark-util-sanitize-uri": "^2.0.0",
|
||||
"micromark-util-subtokenize": "^2.0.0",
|
||||
"micromark-util-symbol": "^2.0.0",
|
||||
"micromark-util-types": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-core-commonmark": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz",
|
||||
"integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "GitHub Sponsors",
|
||||
"url": "https://github.com/sponsors/unifiedjs"
|
||||
},
|
||||
{
|
||||
"type": "OpenCollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"decode-named-character-reference": "^1.0.0",
|
||||
"devlop": "^1.0.0",
|
||||
"micromark-factory-destination": "^2.0.0",
|
||||
"micromark-factory-label": "^2.0.0",
|
||||
"micromark-factory-space": "^2.0.0",
|
||||
"micromark-factory-title": "^2.0.0",
|
||||
"micromark-factory-whitespace": "^2.0.0",
|
||||
"micromark-util-character": "^2.0.0",
|
||||
"micromark-util-chunked": "^2.0.0",
|
||||
"micromark-util-classify-character": "^2.0.0",
|
||||
"micromark-util-html-tag-name": "^2.0.0",
|
||||
"micromark-util-normalize-identifier": "^2.0.0",
|
||||
"micromark-util-resolve-all": "^2.0.0",
|
||||
"micromark-util-subtokenize": "^2.0.0",
|
||||
"micromark-util-symbol": "^2.0.0",
|
||||
"micromark-util-types": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-factory-destination": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz",
|
||||
"integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "GitHub Sponsors",
|
||||
"url": "https://github.com/sponsors/unifiedjs"
|
||||
},
|
||||
{
|
||||
"type": "OpenCollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"micromark-util-character": "^2.0.0",
|
||||
"micromark-util-symbol": "^2.0.0",
|
||||
"micromark-util-types": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-factory-label": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz",
|
||||
"integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "GitHub Sponsors",
|
||||
"url": "https://github.com/sponsors/unifiedjs"
|
||||
},
|
||||
{
|
||||
"type": "OpenCollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"devlop": "^1.0.0",
|
||||
"micromark-util-character": "^2.0.0",
|
||||
"micromark-util-symbol": "^2.0.0",
|
||||
"micromark-util-types": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-factory-space": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz",
|
||||
"integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "GitHub Sponsors",
|
||||
"url": "https://github.com/sponsors/unifiedjs"
|
||||
},
|
||||
{
|
||||
"type": "OpenCollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"micromark-util-character": "^2.0.0",
|
||||
"micromark-util-types": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-factory-title": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz",
|
||||
"integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "GitHub Sponsors",
|
||||
"url": "https://github.com/sponsors/unifiedjs"
|
||||
},
|
||||
{
|
||||
"type": "OpenCollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"micromark-factory-space": "^2.0.0",
|
||||
"micromark-util-character": "^2.0.0",
|
||||
"micromark-util-symbol": "^2.0.0",
|
||||
"micromark-util-types": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-factory-whitespace": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz",
|
||||
"integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "GitHub Sponsors",
|
||||
"url": "https://github.com/sponsors/unifiedjs"
|
||||
},
|
||||
{
|
||||
"type": "OpenCollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"micromark-factory-space": "^2.0.0",
|
||||
"micromark-util-character": "^2.0.0",
|
||||
"micromark-util-symbol": "^2.0.0",
|
||||
"micromark-util-types": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-util-character": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz",
|
||||
"integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "GitHub Sponsors",
|
||||
"url": "https://github.com/sponsors/unifiedjs"
|
||||
},
|
||||
{
|
||||
"type": "OpenCollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"micromark-util-symbol": "^2.0.0",
|
||||
"micromark-util-types": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-util-chunked": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz",
|
||||
"integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "GitHub Sponsors",
|
||||
"url": "https://github.com/sponsors/unifiedjs"
|
||||
},
|
||||
{
|
||||
"type": "OpenCollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"micromark-util-symbol": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-util-classify-character": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz",
|
||||
"integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "GitHub Sponsors",
|
||||
"url": "https://github.com/sponsors/unifiedjs"
|
||||
},
|
||||
{
|
||||
"type": "OpenCollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"micromark-util-character": "^2.0.0",
|
||||
"micromark-util-symbol": "^2.0.0",
|
||||
"micromark-util-types": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-util-combine-extensions": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz",
|
||||
"integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "GitHub Sponsors",
|
||||
"url": "https://github.com/sponsors/unifiedjs"
|
||||
},
|
||||
{
|
||||
"type": "OpenCollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"micromark-util-chunked": "^2.0.0",
|
||||
"micromark-util-types": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-util-decode-numeric-character-reference": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz",
|
||||
"integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "GitHub Sponsors",
|
||||
"url": "https://github.com/sponsors/unifiedjs"
|
||||
},
|
||||
{
|
||||
"type": "OpenCollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"micromark-util-symbol": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-util-encode": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz",
|
||||
"integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "GitHub Sponsors",
|
||||
"url": "https://github.com/sponsors/unifiedjs"
|
||||
},
|
||||
{
|
||||
"type": "OpenCollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/micromark-util-html-tag-name": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz",
|
||||
"integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "GitHub Sponsors",
|
||||
"url": "https://github.com/sponsors/unifiedjs"
|
||||
},
|
||||
{
|
||||
"type": "OpenCollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/micromark-util-normalize-identifier": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz",
|
||||
"integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "GitHub Sponsors",
|
||||
"url": "https://github.com/sponsors/unifiedjs"
|
||||
},
|
||||
{
|
||||
"type": "OpenCollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"micromark-util-symbol": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-util-resolve-all": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz",
|
||||
"integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "GitHub Sponsors",
|
||||
"url": "https://github.com/sponsors/unifiedjs"
|
||||
},
|
||||
{
|
||||
"type": "OpenCollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"micromark-util-types": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-util-sanitize-uri": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz",
|
||||
"integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "GitHub Sponsors",
|
||||
"url": "https://github.com/sponsors/unifiedjs"
|
||||
},
|
||||
{
|
||||
"type": "OpenCollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"micromark-util-character": "^2.0.0",
|
||||
"micromark-util-encode": "^2.0.0",
|
||||
"micromark-util-symbol": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-util-subtokenize": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz",
|
||||
"integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "GitHub Sponsors",
|
||||
"url": "https://github.com/sponsors/unifiedjs"
|
||||
},
|
||||
{
|
||||
"type": "OpenCollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"devlop": "^1.0.0",
|
||||
"micromark-util-chunked": "^2.0.0",
|
||||
"micromark-util-symbol": "^2.0.0",
|
||||
"micromark-util-types": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-util-symbol": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz",
|
||||
"integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "GitHub Sponsors",
|
||||
"url": "https://github.com/sponsors/unifiedjs"
|
||||
},
|
||||
{
|
||||
"type": "OpenCollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/micromark-util-types": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz",
|
||||
"integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "GitHub Sponsors",
|
||||
"url": "https://github.com/sponsors/unifiedjs"
|
||||
},
|
||||
{
|
||||
"type": "OpenCollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/micromatch": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||
|
|
@ -7089,7 +7569,6 @@
|
|||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@
|
|||
"@preact/signals": "^2.9.1",
|
||||
"@tanstack/query-core": "^5.101.0",
|
||||
"@tanstack/virtual-core": "3.17.0",
|
||||
"micromark": "^4.0.2",
|
||||
"preact": "^10.29.2"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import type { ChatAction } from "../../application/state/root-reducer";
|
|||
import type { ChatStateStore } from "../../application/state/store";
|
||||
import type { MessageStreamScrollIntent, MessageStreamVirtualizerHandle } from "../../ui/message-stream/virtualizer";
|
||||
import { MarkdownMessageRenderer } from "../../ui/message-stream/markdown-renderer";
|
||||
import { StreamMarkdownMessageRenderer } from "../../ui/message-stream/stream-markdown-renderer";
|
||||
import { MessageStreamViewport, type MessageStreamViewportState } from "../../ui/message-stream/viewport";
|
||||
import { messageStreamBlocks } from "../../ui/message-stream/stream-blocks";
|
||||
import { messageStreamStateFromShellState, useChatPanelShellState, type ChatPanelMessageStreamShellState } from "../shell-state";
|
||||
|
|
@ -77,14 +78,19 @@ export interface MessageStreamPresenterOptions {
|
|||
}
|
||||
|
||||
export class MessageStreamPresenter {
|
||||
private readonly markdownRenderer: MarkdownMessageRenderer;
|
||||
private readonly obsidianMarkdownRenderer: MarkdownMessageRenderer;
|
||||
private readonly streamMarkdownRenderer: StreamMarkdownMessageRenderer;
|
||||
|
||||
constructor(private readonly options: MessageStreamPresenterOptions) {
|
||||
this.markdownRenderer = new MarkdownMessageRenderer({
|
||||
this.obsidianMarkdownRenderer = new MarkdownMessageRenderer({
|
||||
app: options.obsidian.app,
|
||||
owner: options.obsidian.owner,
|
||||
vaultPath: options.workspace.vaultPath,
|
||||
});
|
||||
this.streamMarkdownRenderer = new StreamMarkdownMessageRenderer({
|
||||
app: options.obsidian.app,
|
||||
vaultPath: options.workspace.vaultPath,
|
||||
});
|
||||
}
|
||||
|
||||
private dispatch(action: ChatAction): void {
|
||||
|
|
@ -114,8 +120,11 @@ export class MessageStreamPresenter {
|
|||
loadOlderTurns: () => {
|
||||
this.options.history.loadOlderTurns();
|
||||
},
|
||||
renderMarkdown: (element, text) => {
|
||||
this.markdownRenderer.renderMarkdown(element, text);
|
||||
renderObsidianMarkdown: (element, text) => {
|
||||
this.obsidianMarkdownRenderer.renderObsidianMarkdown(element, text);
|
||||
},
|
||||
renderStreamMarkdown: (element, text) => {
|
||||
this.streamMarkdownRenderer.renderStreamMarkdown(element, text);
|
||||
},
|
||||
copyMessageText: (text) => void this.copyMessageText(text),
|
||||
actions: this.options.actions,
|
||||
|
|
|
|||
|
|
@ -42,7 +42,8 @@ export interface ChatMessageStreamSurfaceContext {
|
|||
setDisclosureOpen: (bucket: ChatDisclosureBucket, id: string, open: boolean) => void;
|
||||
setForkActionsItem: (itemId: string | null) => void;
|
||||
loadOlderTurns: () => void;
|
||||
renderMarkdown: (element: HTMLElement, text: string) => void;
|
||||
renderObsidianMarkdown: (element: HTMLElement, text: string) => void;
|
||||
renderStreamMarkdown: (element: HTMLElement, text: string) => void;
|
||||
copyMessageText: (text: string) => void;
|
||||
actions: ChatMessageStreamActions;
|
||||
requests: ChatMessageStreamRequests;
|
||||
|
|
@ -52,7 +53,8 @@ export interface MessageStreamSurfaceContextOptions {
|
|||
vaultPath: string;
|
||||
dispatch: (action: ChatAction) => void;
|
||||
loadOlderTurns: () => void;
|
||||
renderMarkdown: (element: HTMLElement, text: string) => void;
|
||||
renderObsidianMarkdown: (element: HTMLElement, text: string) => void;
|
||||
renderStreamMarkdown: (element: HTMLElement, text: string) => void;
|
||||
copyMessageText: (text: string) => void;
|
||||
actions: ChatMessageStreamActions;
|
||||
requests: ChatMessageStreamRequests;
|
||||
|
|
@ -81,7 +83,8 @@ export function createMessageStreamSurfaceContext(options: MessageStreamSurfaceC
|
|||
options.dispatch({ type: "ui/message-fork-actions-set", itemId });
|
||||
},
|
||||
loadOlderTurns: options.loadOlderTurns,
|
||||
renderMarkdown: options.renderMarkdown,
|
||||
renderObsidianMarkdown: options.renderObsidianMarkdown,
|
||||
renderStreamMarkdown: options.renderStreamMarkdown,
|
||||
copyMessageText: options.copyMessageText,
|
||||
actions: options.actions,
|
||||
requests: options.requests,
|
||||
|
|
@ -111,7 +114,8 @@ function messageStreamContextFromProjection(
|
|||
forkActionsItemId: projection.forkActionsItemId,
|
||||
onForkActionsToggle: context.setForkActionsItem,
|
||||
loadOlderTurns: context.loadOlderTurns,
|
||||
renderMarkdown: context.renderMarkdown,
|
||||
renderObsidianMarkdown: context.renderObsidianMarkdown,
|
||||
renderStreamMarkdown: context.renderStreamMarkdown,
|
||||
copyText: context.copyMessageText,
|
||||
onImplementPlan: (target) => {
|
||||
context.actions.implementPlan(target.itemId);
|
||||
|
|
|
|||
|
|
@ -63,27 +63,29 @@ export interface MessageStreamTextView {
|
|||
body: string;
|
||||
className: string;
|
||||
contentKey: string;
|
||||
contentMode: "markdown" | "text";
|
||||
renderMode: MessageStreamTextRenderMode;
|
||||
collapsible: boolean;
|
||||
copyText?: string;
|
||||
actions: MessageStreamTextActions;
|
||||
metadata: MessageStreamTextMetadataView;
|
||||
}
|
||||
|
||||
type MessageStreamTextRenderMode = "text" | "streamMarkdown" | "obsidianMarkdown";
|
||||
|
||||
export function messageStreamTextView(
|
||||
item: MessageStreamItem,
|
||||
annotations?: MessageStreamItemAnnotations,
|
||||
options: { activeTurnId?: string | null; actions?: MessageStreamTextActions } = {},
|
||||
): MessageStreamTextView {
|
||||
const contentMode = textContentMode(item);
|
||||
const renderMode = textRenderMode(item);
|
||||
const body = bodyForTextItem(item);
|
||||
return {
|
||||
id: item.id,
|
||||
roleLabel: roleLabelForTextItem(item),
|
||||
body,
|
||||
className: `${textItemClass(item)}${executionClassName(item.executionState ?? null)}`,
|
||||
contentKey: `${item.id}\u001f${contentMode}`,
|
||||
contentMode,
|
||||
contentKey: `${item.id}\u001f${renderMode}`,
|
||||
renderMode,
|
||||
collapsible: item.kind === "message" && item.role === "user",
|
||||
...definedProp("copyText", copyTextForTextItem(item, options.activeTurnId ?? null)),
|
||||
actions: options.actions ?? {},
|
||||
|
|
@ -91,8 +93,10 @@ export function messageStreamTextView(
|
|||
};
|
||||
}
|
||||
|
||||
function textContentMode(item: MessageStreamItem): "markdown" | "text" {
|
||||
return item.kind === "message" && (item.messageKind !== "proposedPlan" || item.messageState === "completed") ? "markdown" : "text";
|
||||
function textRenderMode(item: MessageStreamItem): MessageStreamTextRenderMode {
|
||||
if (item.kind !== "message") return "text";
|
||||
if (item.messageKind === "assistantResponse" && item.messageState === "streaming") return "streamMarkdown";
|
||||
return item.messageKind !== "proposedPlan" || item.messageState === "completed" ? "obsidianMarkdown" : "text";
|
||||
}
|
||||
|
||||
function bodyForTextItem(item: MessageStreamItem): string {
|
||||
|
|
|
|||
|
|
@ -41,7 +41,8 @@ export interface TextItemDetailStateContext {
|
|||
}
|
||||
|
||||
export interface TextItemContentContext extends TextItemDetailStateContext {
|
||||
renderMarkdown: (parent: HTMLElement, text: string) => void;
|
||||
renderObsidianMarkdown: (parent: HTMLElement, text: string) => void;
|
||||
renderStreamMarkdown: (parent: HTMLElement, text: string) => void;
|
||||
}
|
||||
|
||||
export interface TextItemActionContext extends TextItemDetailStateContext {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ export class MarkdownMessageRenderer {
|
|||
|
||||
constructor(private readonly options: MarkdownMessageRendererOptions) {}
|
||||
|
||||
renderMarkdown(parent: HTMLElement, text: string): void {
|
||||
renderObsidianMarkdown(parent: HTMLElement, text: string): void {
|
||||
const sourcePath = this.options.app.workspace.getActiveFile()?.path ?? "";
|
||||
const generation = (this.renderGenerations.get(parent) ?? 0) + 1;
|
||||
this.renderGenerations.set(parent, generation);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
import { Notice, type App } from "obsidian";
|
||||
import { micromark } from "micromark";
|
||||
|
||||
import { isAbsoluteFileHref, vaultFileLinkTarget, vaultRelativeFileLinkTarget } from "../../../../shared/obsidian/file-links";
|
||||
|
||||
export interface StreamMarkdownMessageRendererOptions {
|
||||
app: App;
|
||||
vaultPath: string;
|
||||
}
|
||||
|
||||
export class StreamMarkdownMessageRenderer {
|
||||
constructor(private readonly options: StreamMarkdownMessageRendererOptions) {}
|
||||
|
||||
renderStreamMarkdown(parent: HTMLElement, text: string): void {
|
||||
const html = micromark(text);
|
||||
const DOMParserConstructor = parent.ownerDocument.defaultView?.DOMParser ?? DOMParser;
|
||||
const parser = new DOMParserConstructor();
|
||||
const parsed = parser.parseFromString(html, "text/html");
|
||||
const nodes = Array.from(parsed.body.childNodes).map((node) => parent.ownerDocument.importNode(node, true));
|
||||
parent.replaceChildren(...nodes);
|
||||
bindStreamMarkdownFileLinks(parent, this.options);
|
||||
}
|
||||
}
|
||||
|
||||
function bindStreamMarkdownFileLinks(parent: HTMLElement, context: StreamMarkdownMessageRendererOptions): void {
|
||||
const sourcePath = context.app.workspace.getActiveFile()?.path ?? "";
|
||||
parent.querySelectorAll<HTMLAnchorElement>("a[href]").forEach((link) => {
|
||||
const href = link.getAttribute("href") ?? "";
|
||||
const target = vaultFileLinkTarget(context.app, context.vaultPath, href);
|
||||
if (target) {
|
||||
link.addClass("codex-panel__filelink");
|
||||
link.onclick = (event) => {
|
||||
event.preventDefault();
|
||||
void context.app.workspace.openLinkText(target, sourcePath, false);
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
const vaultRelativeTarget = vaultRelativeFileLinkTarget(context.vaultPath, context.app.vault.configDir, href);
|
||||
if (vaultRelativeTarget) {
|
||||
link.onclick = (event) => {
|
||||
event.preventDefault();
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isAbsoluteFileHref(href)) return;
|
||||
link.onclick = (event) => {
|
||||
event.preventDefault();
|
||||
new Notice("Cannot open files outside the vault.");
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
@ -25,7 +25,7 @@ export function CollapsibleTextContent({ view, context }: { view: MessageStreamT
|
|||
return () => {
|
||||
content.removeEventListener(MESSAGE_CONTENT_RENDERED_EVENT, update);
|
||||
};
|
||||
}, [view.id, view.body, view.contentMode]);
|
||||
}, [view.id, view.body, view.renderMode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!overflows || !expanded) return;
|
||||
|
|
@ -76,7 +76,7 @@ interface TextContentProps {
|
|||
}
|
||||
|
||||
export function TextContent({ view, context, contentRef, collapsed = false }: TextContentProps): UiNode {
|
||||
const rendersMarkdown = view.contentMode === "markdown";
|
||||
const rendersMarkdown = view.renderMode !== "text";
|
||||
const text = view.body;
|
||||
const localRef = useRef<HTMLDivElement | null>(null);
|
||||
const contextRef = useRef(context);
|
||||
|
|
@ -87,12 +87,14 @@ export function TextContent({ view, context, contentRef, collapsed = false }: Te
|
|||
const content = localRef.current;
|
||||
if (!content) return;
|
||||
const currentContext = contextRef.current;
|
||||
if (rendersMarkdown) {
|
||||
currentContext.renderMarkdown(content, text);
|
||||
if (view.renderMode === "obsidianMarkdown") {
|
||||
currentContext.renderObsidianMarkdown(content, text);
|
||||
} else if (view.renderMode === "streamMarkdown") {
|
||||
currentContext.renderStreamMarkdown(content, text);
|
||||
} else {
|
||||
content.textContent = text;
|
||||
}
|
||||
}, [view.contentMode, rendersMarkdown, text]);
|
||||
}, [view.renderMode, text]);
|
||||
return (
|
||||
<div
|
||||
ref={(element) => {
|
||||
|
|
|
|||
|
|
@ -582,7 +582,8 @@ function messageStreamSurfaceContext(options: {
|
|||
options.dispatch({ type: "ui/message-fork-actions-set", itemId });
|
||||
},
|
||||
loadOlderTurns: vi.fn(),
|
||||
renderMarkdown: vi.fn(),
|
||||
renderObsidianMarkdown: vi.fn(),
|
||||
renderStreamMarkdown: vi.fn(),
|
||||
copyMessageText: vi.fn(),
|
||||
actions: {
|
||||
rollbackThread: vi.fn(),
|
||||
|
|
@ -637,7 +638,7 @@ async function renderedInternalLink(
|
|||
vaultPath: context.vaultPath,
|
||||
});
|
||||
|
||||
markdownRenderer.renderMarkdown(parent, "[[Link]]");
|
||||
markdownRenderer.renderObsidianMarkdown(parent, "[[Link]]");
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
renderMarkdown.mockRestore();
|
||||
|
|
|
|||
|
|
@ -615,7 +615,7 @@ describe("message stream rendering and message actions", () => {
|
|||
forkActionsItemId: null,
|
||||
loadOlderTurns: vi.fn(),
|
||||
renderMarkdown: (element: HTMLElement, text: string) => {
|
||||
markdownRenderer.renderMarkdown(element, text);
|
||||
markdownRenderer.renderObsidianMarkdown(element, text);
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -670,24 +670,15 @@ describe("message stream rendering and message actions", () => {
|
|||
unmountUiRootInAct(parent);
|
||||
});
|
||||
|
||||
it("ignores stale async markdown renders after streaming content updates", async () => {
|
||||
it("uses stream markdown for streaming assistant responses without calling the Obsidian renderer", async () => {
|
||||
const parent = document.createElement("div");
|
||||
const firstRender = deferred<undefined>();
|
||||
const renderMarkdown = vi.spyOn(MarkdownRenderer, "render");
|
||||
renderMarkdown
|
||||
.mockImplementationOnce((_app, text: string, element: HTMLElement) =>
|
||||
firstRender.promise.then(() => {
|
||||
element.textContent = `stale:${text}`;
|
||||
}),
|
||||
)
|
||||
.mockImplementation((_app, text: string, element: HTMLElement) => {
|
||||
element.textContent = `fresh:${text}`;
|
||||
return Promise.resolve();
|
||||
});
|
||||
const markdownRenderer = new MarkdownMessageRenderer({
|
||||
app: { workspace: { getActiveFile: vi.fn(() => null) } } as never,
|
||||
owner: {} as never,
|
||||
vaultPath: "/vault",
|
||||
renderMarkdown.mockImplementation((_app, text: string, element: HTMLElement) => {
|
||||
element.textContent = `obsidian:${text}`;
|
||||
return Promise.resolve();
|
||||
});
|
||||
const renderStreamMarkdown = vi.fn((element: HTMLElement, text: string) => {
|
||||
element.replaceChildren(element.createEl("strong", { text: `stream:${text.replace(/\*/g, "")}` }));
|
||||
});
|
||||
const baseContext = {
|
||||
activeThreadId: "thread",
|
||||
|
|
@ -697,9 +688,8 @@ describe("message stream rendering and message actions", () => {
|
|||
disclosures: emptyDisclosures(),
|
||||
forkActionsItemId: null,
|
||||
loadOlderTurns: vi.fn(),
|
||||
renderMarkdown: (element: HTMLElement, text: string) => {
|
||||
markdownRenderer.renderMarkdown(element, text);
|
||||
},
|
||||
renderObsidianMarkdown: vi.fn(),
|
||||
renderStreamMarkdown,
|
||||
};
|
||||
|
||||
renderMessageStreamBlocksInAct(
|
||||
|
|
@ -719,34 +709,59 @@ describe("message stream rendering and message actions", () => {
|
|||
],
|
||||
}),
|
||||
);
|
||||
const staleContent = expectPresent(parent.querySelector<HTMLElement>(".codex-panel__message-content"));
|
||||
|
||||
expect(parent.querySelector(".codex-panel__message-content")?.innerHTML).toBe("<strong>stream:old</strong>");
|
||||
expect(renderStreamMarkdown).toHaveBeenCalledWith(expect.any(HTMLElement), "old");
|
||||
expect(baseContext.renderObsidianMarkdown).not.toHaveBeenCalled();
|
||||
expect(renderMarkdown).not.toHaveBeenCalled();
|
||||
renderMarkdown.mockRestore();
|
||||
unmountUiRootInAct(parent);
|
||||
});
|
||||
|
||||
it("uses Obsidian markdown for completed assistant responses", async () => {
|
||||
const parent = document.createElement("div");
|
||||
const renderMarkdown = vi.spyOn(MarkdownRenderer, "render");
|
||||
renderMarkdown.mockImplementationOnce((_app, text: string, element: HTMLElement) => {
|
||||
element.textContent = `obsidian:${text}`;
|
||||
return Promise.resolve();
|
||||
});
|
||||
const markdownRenderer = new MarkdownMessageRenderer({
|
||||
app: { workspace: { getActiveFile: vi.fn(() => null) } } as never,
|
||||
owner: {} as never,
|
||||
vaultPath: "/vault",
|
||||
});
|
||||
|
||||
renderMessageStreamBlocksInAct(
|
||||
parent,
|
||||
messageStreamBlocks({
|
||||
...baseContext,
|
||||
activeThreadId: "thread",
|
||||
turnLifecycle: idleTurnLifecycle(),
|
||||
historyCursor: null,
|
||||
loadingHistory: false,
|
||||
disclosures: emptyDisclosures(),
|
||||
forkActionsItemId: null,
|
||||
loadOlderTurns: vi.fn(),
|
||||
renderObsidianMarkdown: (element: HTMLElement, text: string) => {
|
||||
markdownRenderer.renderObsidianMarkdown(element, text);
|
||||
},
|
||||
renderStreamMarkdown: vi.fn(),
|
||||
items: [
|
||||
{
|
||||
id: "a1",
|
||||
kind: "message",
|
||||
role: "assistant",
|
||||
text: "new",
|
||||
text: "**done**",
|
||||
turnId: "turn-1",
|
||||
messageKind: "assistantResponse",
|
||||
messageState: "streaming",
|
||||
messageState: "completed",
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
await Promise.resolve();
|
||||
expect(parent.querySelector(".codex-panel__message-content")?.textContent).toBe("fresh:new");
|
||||
|
||||
firstRender.resolve(undefined);
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
|
||||
expect(staleContent.isConnected).toBe(true);
|
||||
expect(parent.querySelector(".codex-panel__message-content")?.textContent).toBe("fresh:new");
|
||||
expect(parent.querySelector(".codex-panel__message-content")?.textContent).toBe("obsidian:**done**");
|
||||
expect(renderMarkdown).toHaveBeenCalledOnce();
|
||||
renderMarkdown.mockRestore();
|
||||
unmountUiRootInAct(parent);
|
||||
});
|
||||
|
|
@ -772,8 +787,8 @@ describe("message stream rendering and message actions", () => {
|
|||
vaultPath: "/vault",
|
||||
});
|
||||
|
||||
markdownRenderer.renderMarkdown(parent, "old");
|
||||
markdownRenderer.renderMarkdown(parent, "new");
|
||||
markdownRenderer.renderObsidianMarkdown(parent, "old");
|
||||
markdownRenderer.renderObsidianMarkdown(parent, "new");
|
||||
await Promise.resolve();
|
||||
expect(parent.textContent).toBe("fresh:new");
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,96 @@
|
|||
// @vitest-environment jsdom
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { TFile } from "obsidian";
|
||||
|
||||
import { StreamMarkdownMessageRenderer } from "../../../../../src/features/chat/ui/message-stream/stream-markdown-renderer";
|
||||
import { notices } from "../../../../mocks/obsidian";
|
||||
import { installObsidianDomShims } from "../../../../support/dom";
|
||||
|
||||
installObsidianDomShims();
|
||||
|
||||
describe("StreamMarkdownMessageRenderer", () => {
|
||||
beforeEach(() => {
|
||||
notices.length = 0;
|
||||
});
|
||||
|
||||
it("renders basic markdown through micromark", () => {
|
||||
const renderer = streamMarkdownRenderer();
|
||||
const parent = document.createElement("div");
|
||||
|
||||
renderer.renderStreamMarkdown(parent, "# Title\n\n**bold** text");
|
||||
|
||||
expect(parent.querySelector("h1")?.textContent).toBe("Title");
|
||||
expect(parent.querySelector("strong")?.textContent).toBe("bold");
|
||||
expect(parent.textContent).toBe("Title\nbold text");
|
||||
});
|
||||
|
||||
it("leaves wikilinks as plain text while streaming", () => {
|
||||
const renderer = streamMarkdownRenderer();
|
||||
const parent = document.createElement("div");
|
||||
|
||||
renderer.renderStreamMarkdown(parent, "[[Project Note]]");
|
||||
|
||||
expect(parent.textContent).toBe("[[Project Note]]");
|
||||
expect(parent.querySelector("a")).toBeNull();
|
||||
});
|
||||
|
||||
it("opens safe vault file links through Obsidian", () => {
|
||||
const openLinkText = vi.fn();
|
||||
const renderer = streamMarkdownRenderer({ openLinkText, vaultPath: "/Users/showhey/Vault", vaultFiles: ["docs/Guide.md"] });
|
||||
const parent = document.createElement("div");
|
||||
|
||||
renderer.renderStreamMarkdown(parent, "[Guide](/Users/showhey/Vault/docs/Guide.md)");
|
||||
parent.querySelector<HTMLAnchorElement>("a")?.click();
|
||||
|
||||
expect(openLinkText).toHaveBeenCalledWith("docs/Guide.md", "Inbox.md", false);
|
||||
});
|
||||
|
||||
it("does not open absolute file links outside the vault", () => {
|
||||
const openLinkText = vi.fn();
|
||||
const renderer = streamMarkdownRenderer({ openLinkText, vaultPath: "/Users/showhey/Vault" });
|
||||
const parent = document.createElement("div");
|
||||
|
||||
renderer.renderStreamMarkdown(parent, "[Readme](/Users/showhey/Other/README.md)");
|
||||
parent.querySelector<HTMLAnchorElement>("a")?.click();
|
||||
|
||||
expect(openLinkText).not.toHaveBeenCalled();
|
||||
expect(notices).toEqual(["Cannot open files outside the vault."]);
|
||||
});
|
||||
|
||||
it("prevents unresolved vault file links from navigating", () => {
|
||||
const openLinkText = vi.fn();
|
||||
const renderer = streamMarkdownRenderer({ openLinkText, vaultPath: "/Users/showhey/Vault" });
|
||||
const parent = document.createElement("div");
|
||||
|
||||
renderer.renderStreamMarkdown(parent, "[Missing](/Users/showhey/Vault/docs/Missing.md)");
|
||||
const event = new MouseEvent("click", { bubbles: true, cancelable: true });
|
||||
parent.querySelector<HTMLAnchorElement>("a")?.dispatchEvent(event);
|
||||
|
||||
expect(openLinkText).not.toHaveBeenCalled();
|
||||
expect(event.defaultPrevented).toBe(true);
|
||||
expect(notices).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
function streamMarkdownRenderer(options: { openLinkText?: ReturnType<typeof vi.fn>; vaultPath?: string; vaultFiles?: string[] } = {}) {
|
||||
const files = new Map((options.vaultFiles ?? []).map((path) => [path, tFile(path)]));
|
||||
return new StreamMarkdownMessageRenderer({
|
||||
app: {
|
||||
workspace: {
|
||||
getActiveFile: vi.fn(() => tFile("Inbox.md")),
|
||||
openLinkText: options.openLinkText ?? vi.fn(),
|
||||
},
|
||||
vault: {
|
||||
configDir: "vault-config",
|
||||
getAbstractFileByPath: (path: string) => files.get(path) ?? null,
|
||||
},
|
||||
} as never,
|
||||
vaultPath: options.vaultPath ?? "/vault",
|
||||
});
|
||||
}
|
||||
|
||||
function tFile(path: string): TFile {
|
||||
const basename = path.split("/").pop()?.replace(/\.md$/, "") ?? path;
|
||||
return Object.assign(new TFile(), { path, basename });
|
||||
}
|
||||
|
|
@ -59,8 +59,12 @@ function messageStreamBlockItemsEmpty(context: TestMessageStreamContext): boolea
|
|||
return (context.stableItems?.length ?? 0) === 0 && (context.activeItems?.length ?? 0) === 0;
|
||||
}
|
||||
|
||||
type TestMessageStreamContext = Omit<MessageStreamContext, "disclosures" | "forkActionsItemId"> &
|
||||
Partial<Pick<MessageStreamContext, "disclosures" | "forkActionsItemId">> & {
|
||||
type TestMessageStreamContext = Omit<
|
||||
MessageStreamContext,
|
||||
"disclosures" | "forkActionsItemId" | "renderObsidianMarkdown" | "renderStreamMarkdown"
|
||||
> &
|
||||
Partial<Pick<MessageStreamContext, "disclosures" | "forkActionsItemId" | "renderObsidianMarkdown" | "renderStreamMarkdown">> & {
|
||||
renderMarkdown?: (parent: HTMLElement, text: string) => void;
|
||||
turnLifecycle: MessageStreamTurnLifecycleState;
|
||||
historyCursor: string | null;
|
||||
loadingHistory: boolean;
|
||||
|
|
@ -94,10 +98,19 @@ export function testDisclosures(
|
|||
}
|
||||
|
||||
function normalizeMessageStreamContext(context: TestMessageStreamContext): MessageStreamContext {
|
||||
const renderObsidianMarkdown =
|
||||
context.renderObsidianMarkdown ??
|
||||
context.renderMarkdown ??
|
||||
((parent: HTMLElement, text: string) => {
|
||||
parent.createDiv({ text });
|
||||
});
|
||||
const renderStreamMarkdown = context.renderStreamMarkdown ?? context.renderMarkdown ?? renderObsidianMarkdown;
|
||||
return {
|
||||
...context,
|
||||
disclosures: context.disclosures ?? emptyDisclosures(),
|
||||
forkActionsItemId: context.forkActionsItemId ?? null,
|
||||
renderObsidianMarkdown,
|
||||
renderStreamMarkdown,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue