diff --git a/bun.lock b/bun.lock index f6c158cc..2c59deef 100644 --- a/bun.lock +++ b/bun.lock @@ -1,6 +1,5 @@ { "lockfileVersion": 1, - "configVersion": 1, "workspaces": { "": { "name": "hapi", @@ -112,6 +111,7 @@ "diff": "^8.0.2", "hast-util-to-jsx-runtime": "^2.3.6", "katex": "^0.16.45", + "mermaid": "^11.12.0", "react": "^19.2.3", "react-dom": "^19.2.3", "rehype-katex": "^7.0.1", @@ -493,15 +493,15 @@ "@builder.io/vite-plugin-jsx-loc": ["@builder.io/vite-plugin-jsx-loc@0.1.1", "", { "dependencies": { "@builder.io/jsx-loc-internals": "0.0.1" }, "peerDependencies": { "vite": "^4.0.0 || ^5.0.0" } }, "sha512-iAHFkaLBDJBC+EkGO1hF7hnIW2+oKKYVOl8NFAQH//3xeNEzvGdS9tOALRPR+JjR/M5NLyj+FG0VV7WFb1aJmw=="], - "@chevrotain/cst-dts-gen": ["@chevrotain/cst-dts-gen@11.0.3", "", { "dependencies": { "@chevrotain/gast": "11.0.3", "@chevrotain/types": "11.0.3", "lodash-es": "4.17.21" } }, "sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ=="], + "@chevrotain/cst-dts-gen": ["@chevrotain/cst-dts-gen@12.0.0", "", { "dependencies": { "@chevrotain/gast": "12.0.0", "@chevrotain/types": "12.0.0" } }, "sha512-fSL4KXjTl7cDgf0B5Rip9Q05BOrYvkJV/RrBTE/bKDN096E4hN/ySpcBK5B24T76dlQ2i32Zc3PAE27jFnFrKg=="], - "@chevrotain/gast": ["@chevrotain/gast@11.0.3", "", { "dependencies": { "@chevrotain/types": "11.0.3", "lodash-es": "4.17.21" } }, "sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q=="], + "@chevrotain/gast": ["@chevrotain/gast@12.0.0", "", { "dependencies": { "@chevrotain/types": "12.0.0" } }, "sha512-1ne/m3XsIT8aEdrvT33so0GUC+wkctpUPK6zU9IlOyJLUbR0rg4G7ZiApiJbggpgPir9ERy3FRjT6T7lpgetnQ=="], "@chevrotain/regexp-to-ast": ["@chevrotain/regexp-to-ast@12.0.0", "", {}, "sha512-p+EW9MaJwgaHguhoqwOtx/FwuGr+DnNn857sXWOi/mClXIkPGl3rn7hGNWvo31HA3vyeQxjqe+H36yZJwYU8cA=="], - "@chevrotain/types": ["@chevrotain/types@11.0.3", "", {}, "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ=="], + "@chevrotain/types": ["@chevrotain/types@12.0.0", "", {}, "sha512-S+04vjFQKeuYw0/eW3U52LkAHQsB1ASxsPGsLPUyQgrZ2iNNibQrsidruDzjEX2JYfespXMG0eZmXlhA6z7nWA=="], - "@chevrotain/utils": ["@chevrotain/utils@11.0.3", "", {}, "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ=="], + "@chevrotain/utils": ["@chevrotain/utils@12.0.0", "", {}, "sha512-lB59uJoaGIfOOL9knQqQRfhl9g7x8/wqFkp13zTdkRu1huG9kg6IJs1O8hqj9rs6h7orGxHJUKb+mX3rPbWGhA=="], "@csstools/color-helpers": ["@csstools/color-helpers@5.1.0", "", {}, "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA=="], @@ -701,7 +701,7 @@ "@medv/finder": ["@medv/finder@4.0.2", "", {}, "sha512-RraNY9SCcx4KZV0Dh6BEW6XEW2swkqYca74pkFFRw6hHItSHiy+O/xMnpbofjYbzXj0tSpBGthUF1hHTsr3vIQ=="], - "@mermaid-js/parser": ["@mermaid-js/parser@0.6.3", "", { "dependencies": { "langium": "3.3.1" } }, "sha512-lnjOhe7zyHjc+If7yT4zoedx2vo4sHaTmtkl1+or8BRTnCtDmcTpAjpzDSfCZrshM5bCoz0GyidzadJAH1xobA=="], + "@mermaid-js/parser": ["@mermaid-js/parser@1.1.0", "", { "dependencies": { "langium": "^4.0.0" } }, "sha512-gxK9ZX2+Fex5zu8LhRQoMeMPEHbc73UKZ0FQ54YrQtUxE1VVhMwzeNtKRPAu5aXks4FasbMe4xB4bWrmq6Jlxw=="], "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.1", "", { "dependencies": { "@hono/node-server": "^1.19.7", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-yO28oVFFC7EBoiKdAn+VqRm+plcfv4v0xp6osG/VsCB0NlPZWi87ajbCZZ8f/RvOFLEu7//rSRmuZZ7lMoe3gQ=="], @@ -1421,9 +1421,9 @@ "check-error": ["check-error@2.1.1", "", {}, "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw=="], - "chevrotain": ["chevrotain@11.0.3", "", { "dependencies": { "@chevrotain/cst-dts-gen": "11.0.3", "@chevrotain/gast": "11.0.3", "@chevrotain/regexp-to-ast": "11.0.3", "@chevrotain/types": "11.0.3", "@chevrotain/utils": "11.0.3", "lodash-es": "4.17.21" } }, "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw=="], + "chevrotain": ["chevrotain@12.0.0", "", { "dependencies": { "@chevrotain/cst-dts-gen": "12.0.0", "@chevrotain/gast": "12.0.0", "@chevrotain/regexp-to-ast": "12.0.0", "@chevrotain/types": "12.0.0", "@chevrotain/utils": "12.0.0" } }, "sha512-csJvb+6kEiQaqo1woTdSAuOWdN0WTLIydkKrBnS+V5gZz0oqBrp4kQ35519QgK6TpBThiG3V1vNSHlIkv4AglQ=="], - "chevrotain-allstar": ["chevrotain-allstar@0.3.1", "", { "dependencies": { "lodash-es": "^4.17.21" }, "peerDependencies": { "chevrotain": "^11.0.0" } }, "sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw=="], + "chevrotain-allstar": ["chevrotain-allstar@0.4.1", "", { "dependencies": { "lodash-es": "^4.17.21" }, "peerDependencies": { "chevrotain": "^12.0.0" } }, "sha512-PvVJm3oGqrveUVW2Vt/eZGeiAIsJszYweUcYwcskg9e+IubNYKKD+rHHem7A6XVO22eDAL+inxNIGAzZ/VIWlA=="], "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], @@ -1575,7 +1575,7 @@ "d3-zoom": ["d3-zoom@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "2 - 3", "d3-transition": "2 - 3" } }, "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw=="], - "dagre-d3-es": ["dagre-d3-es@7.0.13", "", { "dependencies": { "d3": "^7.9.0", "lodash-es": "^4.17.21" } }, "sha512-efEhnxpSuwpYOKRm/L5KbqoZmNNukHa/Flty4Wp62JRvgH2ojwVgPgdYyr4twpieZnyRDdIH7PY2mopX26+j2Q=="], + "dagre-d3-es": ["dagre-d3-es@7.0.14", "", { "dependencies": { "d3": "^7.9.0", "lodash-es": "^4.17.21" } }, "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg=="], "data-urls": ["data-urls@5.0.0", "", { "dependencies": { "whatwg-mimetype": "^4.0.0", "whatwg-url": "^14.0.0" } }, "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg=="], @@ -2087,7 +2087,7 @@ "khroma": ["khroma@2.1.0", "", {}, "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw=="], - "langium": ["langium@3.3.1", "", { "dependencies": { "chevrotain": "~11.0.3", "chevrotain-allstar": "~0.3.0", "vscode-languageserver": "~9.0.1", "vscode-languageserver-textdocument": "~1.0.11", "vscode-uri": "~3.0.8" } }, "sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w=="], + "langium": ["langium@4.2.2", "", { "dependencies": { "@chevrotain/regexp-to-ast": "~12.0.0", "chevrotain": "~12.0.0", "chevrotain-allstar": "~0.4.1", "vscode-languageserver": "~9.0.1", "vscode-languageserver-textdocument": "~1.0.11", "vscode-uri": "~3.1.0" } }, "sha512-JUshTRAfHI4/MF9dH2WupvjSXyn8JBuUEWazB8ZVJUtXutT0doDlAv1XKbZ1Pb5sMexa8FF4CFBc0iiul7gbUQ=="], "layout-base": ["layout-base@1.0.2", "", {}, "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg=="], @@ -2135,7 +2135,7 @@ "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], - "lodash-es": ["lodash-es@4.17.22", "", {}, "sha512-XEawp1t0gxSi9x01glktRZ5HDy0HXqrM0x5pXQM98EaI0NxO6jVM7omDOxsuEo5UIASAnm2bRp1Jt/e0a2XU8Q=="], + "lodash-es": ["lodash-es@4.18.1", "", {}, "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A=="], "lodash.debounce": ["lodash.debounce@4.0.8", "", {}, "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="], @@ -2211,7 +2211,7 @@ "merge-value": ["merge-value@1.0.0", "", { "dependencies": { "get-value": "^2.0.6", "is-extendable": "^1.0.0", "mixin-deep": "^1.2.0", "set-value": "^2.0.0" } }, "sha512-fJMmvat4NeKz63Uv9iHWcPDjCWcCkoiRoajRTEO8hlhUC6rwaHg0QCF9hBOTjZmm4JuglPckPSTtcuJL5kp0TQ=="], - "mermaid": ["mermaid@11.12.2", "", { "dependencies": { "@braintree/sanitize-url": "^7.1.1", "@iconify/utils": "^3.0.1", "@mermaid-js/parser": "^0.6.3", "@types/d3": "^7.4.3", "cytoscape": "^3.29.3", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.13", "dayjs": "^1.11.18", "dompurify": "^3.2.5", "katex": "^0.16.22", "khroma": "^2.1.0", "lodash-es": "^4.17.21", "marked": "^16.2.1", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", "uuid": "^11.1.0" } }, "sha512-n34QPDPEKmaeCG4WDMGy0OT6PSyxKCfy2pJgShP+Qow2KLrvWjclwbc3yXfSIf4BanqWEhQEpngWwNp/XhZt6w=="], + "mermaid": ["mermaid@11.14.0", "", { "dependencies": { "@braintree/sanitize-url": "^7.1.1", "@iconify/utils": "^3.0.2", "@mermaid-js/parser": "^1.1.0", "@types/d3": "^7.4.3", "@upsetjs/venn.js": "^2.0.0", "cytoscape": "^3.33.1", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.14", "dayjs": "^1.11.19", "dompurify": "^3.3.1", "katex": "^0.16.25", "khroma": "^2.1.0", "lodash-es": "^4.17.23", "marked": "^16.3.0", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", "uuid": "^11.1.0" } }, "sha512-GSGloRsBs+JINmmhl0JDwjpuezCsHB4WGI4NASHxL3fHo3o/BRXTxhDLKnln8/Q0lRFRyDdEjmk1/d5Sn1Xz8g=="], "methods": ["methods@1.1.2", "", {}, "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="], @@ -2993,7 +2993,7 @@ "utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="], - "uuid": ["uuid@13.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w=="], + "uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], "v8n": ["v8n@1.5.1", "", {}, "sha512-LdabyT4OffkyXFCe9UT+uMkxNBs5rcTVuZClvxQr08D5TUgo1OFKkoT65qYRCsiKBl/usHjpXvP4hHMzzDRj3A=="], @@ -3035,7 +3035,7 @@ "vscode-languageserver-types": ["vscode-languageserver-types@3.17.5", "", {}, "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg=="], - "vscode-uri": ["vscode-uri@3.0.8", "", {}, "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw=="], + "vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="], "vue": ["vue@3.5.26", "", { "dependencies": { "@vue/compiler-dom": "3.5.26", "@vue/compiler-sfc": "3.5.26", "@vue/runtime-dom": "3.5.26", "@vue/server-renderer": "3.5.26", "@vue/shared": "3.5.26" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA=="], @@ -3157,10 +3157,6 @@ "@builder.io/vite-plugin-jsx-loc/vite": ["vite@5.4.21", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw=="], - "@chevrotain/cst-dts-gen/lodash-es": ["lodash-es@4.17.21", "", {}, "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="], - - "@chevrotain/gast/lodash-es": ["lodash-es@4.17.21", "", {}, "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="], - "@emotion/babel-plugin/@emotion/hash": ["@emotion/hash@0.9.2", "", {}, "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g=="], "@emotion/babel-plugin/convert-source-map": ["convert-source-map@1.9.0", "", {}, "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="], @@ -3205,14 +3201,14 @@ "@lobehub/ui/marked": ["marked@17.0.6", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-gB0gkNafnonOw0obSTEGZTT86IuhILt2Wfx0mWH/1Au83kybTayroZ/V6nS25mN7u8ASy+5fMhgB3XPNrOZdmA=="], - "@lobehub/ui/mermaid": ["mermaid@11.14.0", "", { "dependencies": { "@braintree/sanitize-url": "^7.1.1", "@iconify/utils": "^3.0.2", "@mermaid-js/parser": "^1.1.0", "@types/d3": "^7.4.3", "@upsetjs/venn.js": "^2.0.0", "cytoscape": "^3.33.1", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.14", "dayjs": "^1.11.19", "dompurify": "^3.3.1", "katex": "^0.16.25", "khroma": "^2.1.0", "lodash-es": "^4.17.23", "marked": "^16.3.0", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", "uuid": "^11.1.0" } }, "sha512-GSGloRsBs+JINmmhl0JDwjpuezCsHB4WGI4NASHxL3fHo3o/BRXTxhDLKnln8/Q0lRFRyDdEjmk1/d5Sn1Xz8g=="], - "@lobehub/ui/remark-cjk-friendly": ["remark-cjk-friendly@2.0.1", "", { "dependencies": { "micromark-extension-cjk-friendly": "2.0.1" }, "peerDependencies": { "@types/mdast": "^4.0.0", "unified": "^11.0.0" }, "optionalPeers": ["@types/mdast"] }, "sha512-6WwkoQyZf/4j5k53zdFYrR8Ca+UVn992jXdLUSBDZR4eBpFhKyVxmA4gUHra/5fesjGIxrDhHesNr/sVoiiysA=="], "@lobehub/ui/remend": ["remend@1.3.0", "", {}, "sha512-iIhggPkhW3hFImKtB10w0dz4EZbs28mV/dmbcYVonWEJ6UGHHpP+bFZnTh6GNWJONg5m+U56JrL+8IxZRdgWjw=="], "@lobehub/ui/shiki": ["shiki@4.0.2", "", { "dependencies": { "@shikijs/core": "4.0.2", "@shikijs/engine-javascript": "4.0.2", "@shikijs/engine-oniguruma": "4.0.2", "@shikijs/langs": "4.0.2", "@shikijs/themes": "4.0.2", "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-eAVKTMedR5ckPo4xne/PjYQYrU3qx78gtJZ+sHlXEg5IHhhoQhMfZVzetTYuaJS0L2Ef3AcCRzCHV8T0WI6nIQ=="], + "@lobehub/ui/uuid": ["uuid@13.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w=="], + "@mdx-js/mdx/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], "@modelcontextprotocol/sdk/express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], @@ -3417,10 +3413,6 @@ "body-parser/raw-body": ["raw-body@2.5.3", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" } }, "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA=="], - "chevrotain/@chevrotain/regexp-to-ast": ["@chevrotain/regexp-to-ast@11.0.3", "", {}, "sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA=="], - - "chevrotain/lodash-es": ["lodash-es@4.17.21", "", {}, "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="], - "cliui/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=="], "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], @@ -3485,12 +3477,6 @@ "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], - "mermaid/dayjs": ["dayjs@1.11.19", "", {}, "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw=="], - - "mermaid/katex": ["katex@0.16.27", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-aeQoDkuRWSqQN6nSvVCEFvfXdqo1OQiCmmW1kc9xSdjutPv7BGO7pqY9sQRJpMOGrEdfDgF2TfRXe5eUAD2Waw=="], - - "mermaid/uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], - "micromark-extension-math/katex": ["katex@0.16.27", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-aeQoDkuRWSqQN6nSvVCEFvfXdqo1OQiCmmW1kc9xSdjutPv7BGO7pqY9sQRJpMOGrEdfDgF2TfRXe5eUAD2Waw=="], "motion/framer-motion": ["framer-motion@12.38.0", "", { "dependencies": { "motion-dom": "^12.38.0", "motion-utils": "^12.36.0", "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-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g=="], @@ -3553,6 +3539,8 @@ "streamdown/lucide-react": ["lucide-react@0.542.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-w3hD8/SQB7+lzU2r4VdFyzzOzKnUjTZIF/MQJGSSvni7Llewni4vuViRppfRAa2guOsY5k4jZyxw/i9DQHv+dw=="], + "streamdown/mermaid": ["mermaid@11.12.2", "", { "dependencies": { "@braintree/sanitize-url": "^7.1.1", "@iconify/utils": "^3.0.1", "@mermaid-js/parser": "^0.6.3", "@types/d3": "^7.4.3", "cytoscape": "^3.29.3", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.13", "dayjs": "^1.11.18", "dompurify": "^3.2.5", "katex": "^0.16.22", "khroma": "^2.1.0", "lodash-es": "^4.17.21", "marked": "^16.2.1", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", "uuid": "^11.1.0" } }, "sha512-n34QPDPEKmaeCG4WDMGy0OT6PSyxKCfy2pJgShP+Qow2KLrvWjclwbc3yXfSIf4BanqWEhQEpngWwNp/XhZt6w=="], + "string-width-cjs/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], "string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], @@ -3611,16 +3599,6 @@ "@lobehub/ui/@shikijs/transformers/@shikijs/types": ["@shikijs/types@4.0.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-qzbeRooUTPnLE+sHD/Z8DStmaDgnbbc/pMrU203950aRqjX/6AFHeDYT+j00y2lPdz0ywJKx7o/7qnqTivtlXg=="], - "@lobehub/ui/mermaid/@mermaid-js/parser": ["@mermaid-js/parser@1.1.0", "", { "dependencies": { "langium": "^4.0.0" } }, "sha512-gxK9ZX2+Fex5zu8LhRQoMeMPEHbc73UKZ0FQ54YrQtUxE1VVhMwzeNtKRPAu5aXks4FasbMe4xB4bWrmq6Jlxw=="], - - "@lobehub/ui/mermaid/dagre-d3-es": ["dagre-d3-es@7.0.14", "", { "dependencies": { "d3": "^7.9.0", "lodash-es": "^4.17.21" } }, "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg=="], - - "@lobehub/ui/mermaid/lodash-es": ["lodash-es@4.18.1", "", {}, "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A=="], - - "@lobehub/ui/mermaid/marked": ["marked@16.4.2", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA=="], - - "@lobehub/ui/mermaid/uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], - "@lobehub/ui/remark-cjk-friendly/micromark-extension-cjk-friendly": ["micromark-extension-cjk-friendly@2.0.1", "", { "dependencies": { "devlop": "^1.1.0", "micromark-extension-cjk-friendly-util": "3.0.1", "micromark-util-chunked": "^2.0.1", "micromark-util-resolve-all": "^2.0.1", "micromark-util-symbol": "^2.0.1" }, "peerDependencies": { "micromark": "^4.0.0", "micromark-util-types": "^2.0.0" }, "optionalPeers": ["micromark-util-types"] }, "sha512-OkzoYVTL1ChbvQ8Cc1ayTIz7paFQz8iS9oIYmewncweUSwmWR+hkJF9spJ1lxB90XldJl26A1F4IkPOKS3bDXw=="], "@lobehub/ui/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@4.0.2", "", { "dependencies": { "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-7PW0Nm49DcoUIQEXlJhNNBHyoGMjalRETTCcjMqEaMoJRLljy1Bi/EGV3/qLBgLKQejdspiiYuHGQW6dX94Nag=="], @@ -3787,6 +3765,14 @@ "source-map/whatwg-url/webidl-conversions": ["webidl-conversions@4.0.2", "", {}, "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg=="], + "streamdown/mermaid/@mermaid-js/parser": ["@mermaid-js/parser@0.6.3", "", { "dependencies": { "langium": "3.3.1" } }, "sha512-lnjOhe7zyHjc+If7yT4zoedx2vo4sHaTmtkl1+or8BRTnCtDmcTpAjpzDSfCZrshM5bCoz0GyidzadJAH1xobA=="], + + "streamdown/mermaid/dagre-d3-es": ["dagre-d3-es@7.0.13", "", { "dependencies": { "d3": "^7.9.0", "lodash-es": "^4.17.21" } }, "sha512-efEhnxpSuwpYOKRm/L5KbqoZmNNukHa/Flty4Wp62JRvgH2ojwVgPgdYyr4twpieZnyRDdIH7PY2mopX26+j2Q=="], + + "streamdown/mermaid/dayjs": ["dayjs@1.11.19", "", {}, "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw=="], + + "streamdown/mermaid/lodash-es": ["lodash-es@4.17.22", "", {}, "sha512-XEawp1t0gxSi9x01glktRZ5HDy0HXqrM0x5pXQM98EaI0NxO6jVM7omDOxsuEo5UIASAnm2bRp1Jt/e0a2XU8Q=="], + "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-HHB50pdsBX6k47S4u5g/CaLjqS3qwaOVE5ILsq64jyzgMhLuCuZ8rGzM9yhsAjfjkbgUPMzZEPa7DAp7yz6vuA=="], @@ -3973,8 +3959,6 @@ "@floating-ui/react/@floating-ui/react-dom/@floating-ui/dom/@floating-ui/core": ["@floating-ui/core@1.7.5", "", { "dependencies": { "@floating-ui/utils": "^0.2.11" } }, "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ=="], - "@lobehub/ui/mermaid/@mermaid-js/parser/langium": ["langium@4.2.2", "", { "dependencies": { "@chevrotain/regexp-to-ast": "~12.0.0", "chevrotain": "~12.0.0", "chevrotain-allstar": "~0.4.1", "vscode-languageserver": "~9.0.1", "vscode-languageserver-textdocument": "~1.0.11", "vscode-uri": "~3.1.0" } }, "sha512-JUshTRAfHI4/MF9dH2WupvjSXyn8JBuUEWazB8ZVJUtXutT0doDlAv1XKbZ1Pb5sMexa8FF4CFBc0iiul7gbUQ=="], - "@lobehub/ui/remark-cjk-friendly/micromark-extension-cjk-friendly/micromark-extension-cjk-friendly-util": ["micromark-extension-cjk-friendly-util@3.0.1", "", { "dependencies": { "get-east-asian-width": "^1.4.0", "micromark-util-character": "^2.1.1", "micromark-util-symbol": "^2.0.1" } }, "sha512-GcbXqTTHOsiZHyF753oIddP/J2eH8j9zpyQPhkof6B2JNxfEJabnQqxbCgzJNuNes0Y2jTNJ3LiYPSXr6eJA8w=="], "@modelcontextprotocol/sdk/express/accepts/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], @@ -4043,6 +4027,8 @@ "qrcode/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "streamdown/mermaid/@mermaid-js/parser/langium": ["langium@3.3.1", "", { "dependencies": { "chevrotain": "~11.0.3", "chevrotain-allstar": "~0.3.0", "vscode-languageserver": "~9.0.1", "vscode-languageserver-textdocument": "~1.0.11", "vscode-uri": "~3.0.8" } }, "sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w=="], + "vite-node/vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="], "vite-node/vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="], @@ -4139,12 +4125,6 @@ "yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "@lobehub/ui/mermaid/@mermaid-js/parser/langium/chevrotain": ["chevrotain@12.0.0", "", { "dependencies": { "@chevrotain/cst-dts-gen": "12.0.0", "@chevrotain/gast": "12.0.0", "@chevrotain/regexp-to-ast": "12.0.0", "@chevrotain/types": "12.0.0", "@chevrotain/utils": "12.0.0" } }, "sha512-csJvb+6kEiQaqo1woTdSAuOWdN0WTLIydkKrBnS+V5gZz0oqBrp4kQ35519QgK6TpBThiG3V1vNSHlIkv4AglQ=="], - - "@lobehub/ui/mermaid/@mermaid-js/parser/langium/chevrotain-allstar": ["chevrotain-allstar@0.4.1", "", { "dependencies": { "lodash-es": "^4.17.21" }, "peerDependencies": { "chevrotain": "^12.0.0" } }, "sha512-PvVJm3oGqrveUVW2Vt/eZGeiAIsJszYweUcYwcskg9e+IubNYKKD+rHHem7A6XVO22eDAL+inxNIGAzZ/VIWlA=="], - - "@lobehub/ui/mermaid/@mermaid-js/parser/langium/vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="], - "hapi-website/vitest/vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="], "hapi-website/vitest/vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="], @@ -4197,12 +4177,22 @@ "qrcode/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "@lobehub/ui/mermaid/@mermaid-js/parser/langium/chevrotain/@chevrotain/cst-dts-gen": ["@chevrotain/cst-dts-gen@12.0.0", "", { "dependencies": { "@chevrotain/gast": "12.0.0", "@chevrotain/types": "12.0.0" } }, "sha512-fSL4KXjTl7cDgf0B5Rip9Q05BOrYvkJV/RrBTE/bKDN096E4hN/ySpcBK5B24T76dlQ2i32Zc3PAE27jFnFrKg=="], + "streamdown/mermaid/@mermaid-js/parser/langium/chevrotain": ["chevrotain@11.0.3", "", { "dependencies": { "@chevrotain/cst-dts-gen": "11.0.3", "@chevrotain/gast": "11.0.3", "@chevrotain/regexp-to-ast": "11.0.3", "@chevrotain/types": "11.0.3", "@chevrotain/utils": "11.0.3", "lodash-es": "4.17.21" } }, "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw=="], - "@lobehub/ui/mermaid/@mermaid-js/parser/langium/chevrotain/@chevrotain/gast": ["@chevrotain/gast@12.0.0", "", { "dependencies": { "@chevrotain/types": "12.0.0" } }, "sha512-1ne/m3XsIT8aEdrvT33so0GUC+wkctpUPK6zU9IlOyJLUbR0rg4G7ZiApiJbggpgPir9ERy3FRjT6T7lpgetnQ=="], + "streamdown/mermaid/@mermaid-js/parser/langium/chevrotain-allstar": ["chevrotain-allstar@0.3.1", "", { "dependencies": { "lodash-es": "^4.17.21" }, "peerDependencies": { "chevrotain": "^11.0.0" } }, "sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw=="], - "@lobehub/ui/mermaid/@mermaid-js/parser/langium/chevrotain/@chevrotain/types": ["@chevrotain/types@12.0.0", "", {}, "sha512-S+04vjFQKeuYw0/eW3U52LkAHQsB1ASxsPGsLPUyQgrZ2iNNibQrsidruDzjEX2JYfespXMG0eZmXlhA6z7nWA=="], + "streamdown/mermaid/@mermaid-js/parser/langium/vscode-uri": ["vscode-uri@3.0.8", "", {}, "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw=="], - "@lobehub/ui/mermaid/@mermaid-js/parser/langium/chevrotain/@chevrotain/utils": ["@chevrotain/utils@12.0.0", "", {}, "sha512-lB59uJoaGIfOOL9knQqQRfhl9g7x8/wqFkp13zTdkRu1huG9kg6IJs1O8hqj9rs6h7orGxHJUKb+mX3rPbWGhA=="], + "streamdown/mermaid/@mermaid-js/parser/langium/chevrotain/@chevrotain/cst-dts-gen": ["@chevrotain/cst-dts-gen@11.0.3", "", { "dependencies": { "@chevrotain/gast": "11.0.3", "@chevrotain/types": "11.0.3", "lodash-es": "4.17.21" } }, "sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ=="], + + "streamdown/mermaid/@mermaid-js/parser/langium/chevrotain/@chevrotain/gast": ["@chevrotain/gast@11.0.3", "", { "dependencies": { "@chevrotain/types": "11.0.3", "lodash-es": "4.17.21" } }, "sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q=="], + + "streamdown/mermaid/@mermaid-js/parser/langium/chevrotain/@chevrotain/regexp-to-ast": ["@chevrotain/regexp-to-ast@11.0.3", "", {}, "sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA=="], + + "streamdown/mermaid/@mermaid-js/parser/langium/chevrotain/@chevrotain/types": ["@chevrotain/types@11.0.3", "", {}, "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ=="], + + "streamdown/mermaid/@mermaid-js/parser/langium/chevrotain/@chevrotain/utils": ["@chevrotain/utils@11.0.3", "", {}, "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ=="], + + "streamdown/mermaid/@mermaid-js/parser/langium/chevrotain/lodash-es": ["lodash-es@4.17.21", "", {}, "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="], } } diff --git a/cli/src/claude/runClaude.ts b/cli/src/claude/runClaude.ts index c7aae047..9874d833 100644 --- a/cli/src/claude/runClaude.ts +++ b/cli/src/claude/runClaude.ts @@ -282,6 +282,41 @@ export async function runClaude(options: StartOptions = {}): Promise { return; } + if (specialCommand.type === 'plan') { + logger.debug('[start] Detected /plan command'); + currentPermissionMode = specialCommand.mode ?? 'plan'; + currentSessionRef.current?.setPermissionMode(currentPermissionMode); + currentSessionRef.current?.pushKeepAlive(); + session.sendSessionEvent({ + type: 'permission-mode-changed', + mode: currentPermissionMode + }); + + const enhancedMode: EnhancedMode = { + permissionMode: currentPermissionMode, + model: messageModel, + effort: messageEffort, + fallbackModel: messageFallbackModel, + customSystemPrompt: messageCustomSystemPrompt, + appendSystemPrompt: messageAppendSystemPrompt, + allowedTools: messageAllowedTools, + disallowedTools: messageDisallowedTools + }; + + if (!specialCommand.prompt) { + if (localId) { + session.emitMessagesConsumed([localId]); + } + logger.debugLargeJson('[start] /plan command applied without prompt:', message); + return; + } + + const planPrompt = formatMessageWithAttachments(specialCommand.prompt, message.content.attachments); + messageQueue.push(planPrompt, enhancedMode, localId); + logger.debugLargeJson('[start] /plan command prompt pushed to queue:', message); + return; + } + // Push with resolved permission mode, model, system prompts, and tools const enhancedMode: EnhancedMode = { permissionMode: messagePermissionMode ?? 'default', diff --git a/cli/src/parsers/specialCommands.test.ts b/cli/src/parsers/specialCommands.test.ts index ad6a5da2..c38b1997 100644 --- a/cli/src/parsers/specialCommands.test.ts +++ b/cli/src/parsers/specialCommands.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import { parseCompact, parseClear, parseSpecialCommand } from './specialCommands'; +import { parseCompact, parseClear, parsePlan, parseSpecialCommand } from './specialCommands'; describe('parseCompact', () => { it('should parse /compact command with argument', () => { @@ -49,6 +49,49 @@ describe('parseClear', () => { }); }); +describe('parsePlan', () => { + it('should parse /plan without a prompt', () => { + const result = parsePlan('/plan'); + expect(result).toEqual({ + isPlan: true, + mode: 'plan', + prompt: undefined + }); + }); + + it('should parse /plan with a prompt', () => { + const result = parsePlan('/plan 帮我规划五一行程'); + expect(result).toEqual({ + isPlan: true, + mode: 'plan', + prompt: '帮我规划五一行程' + }); + }); + + it('should strip matching quotes around the prompt', () => { + const result = parsePlan('/plan "帮我规划五一行程"'); + expect(result).toEqual({ + isPlan: true, + mode: 'plan', + prompt: '帮我规划五一行程' + }); + }); + + it('should parse /plan off as default mode', () => { + const result = parsePlan('/plan off'); + expect(result).toEqual({ + isPlan: true, + mode: 'default', + prompt: undefined + }); + }); + + it('should not parse partial matches', () => { + expect(parsePlan('/planner test').isPlan).toBe(false); + expect(parsePlan('please /plan this').isPlan).toBe(false); + }); +}); + describe('parseSpecialCommand', () => { it('should detect compact command', () => { const result = parseSpecialCommand('/compact optimize'); @@ -62,6 +105,13 @@ describe('parseSpecialCommand', () => { expect(result.originalMessage).toBeUndefined(); }); + it('should detect plan command with prompt', () => { + const result = parseSpecialCommand('/plan "帮我规划五一行程"'); + expect(result.type).toBe('plan'); + expect(result.mode).toBe('plan'); + expect(result.prompt).toBe('帮我规划五一行程'); + }); + it('should return null for regular messages', () => { const result = parseSpecialCommand('hello world'); expect(result.type).toBeNull(); @@ -72,10 +122,12 @@ describe('parseSpecialCommand', () => { // Test with extra whitespace expect(parseSpecialCommand(' /compact test ').type).toBe('compact'); expect(parseSpecialCommand(' /clear ').type).toBe('clear'); + expect(parseSpecialCommand(' /plan test ').type).toBe('plan'); // Test partial matches should not trigger expect(parseSpecialCommand('some /compact text').type).toBeNull(); expect(parseSpecialCommand('/compactor').type).toBeNull(); expect(parseSpecialCommand('/clearing').type).toBeNull(); + expect(parseSpecialCommand('/planner').type).toBeNull(); }); -}); \ No newline at end of file +}); diff --git a/cli/src/parsers/specialCommands.ts b/cli/src/parsers/specialCommands.ts index 69226460..a9ebcac7 100644 --- a/cli/src/parsers/specialCommands.ts +++ b/cli/src/parsers/specialCommands.ts @@ -11,9 +11,17 @@ export interface ClearCommandResult { isClear: boolean; } +export interface PlanCommandResult { + isPlan: boolean; + mode: 'plan' | 'default'; + prompt?: string; +} + export interface SpecialCommandResult { - type: 'compact' | 'clear' | null; + type: 'compact' | 'clear' | 'plan' | null; originalMessage?: string; + mode?: 'plan' | 'default'; + prompt?: string; } /** @@ -55,6 +63,52 @@ export function parseClear(message: string): ClearCommandResult { }; } +function stripMatchingQuotes(value: string): string { + const trimmed = value.trim(); + if (trimmed.length < 2) { + return trimmed; + } + + const first = trimmed[0]; + const last = trimmed[trimmed.length - 1]; + if ((first === '"' && last === '"') || (first === '\'' && last === '\'')) { + return trimmed.slice(1, -1).trim(); + } + + return trimmed; +} + +/** + * Parse /plan command for remote Claude sessions. + * - /plan: switch to Claude plan permission mode. + * - /plan off: switch back to default mode. + * - /plan : switch to plan mode and send the remaining prompt. + */ +export function parsePlan(message: string): PlanCommandResult { + const trimmed = message.trim(); + const match = /^\/plan(?:\s+([\s\S]*))?$/i.exec(trimmed); + if (!match) { + return { + isPlan: false, + mode: 'plan' + }; + } + + const rawArg = match[1]?.trim() ?? ''; + if (rawArg.toLowerCase() === 'off') { + return { + isPlan: true, + mode: 'default' + }; + } + + return { + isPlan: true, + mode: 'plan', + prompt: rawArg ? stripMatchingQuotes(rawArg) : undefined + }; +} + /** * Unified parser for special commands * Returns the type of command and original message if applicable @@ -74,8 +128,18 @@ export function parseSpecialCommand(message: string): SpecialCommandResult { type: 'clear' }; } + + const planResult = parsePlan(message); + if (planResult.isPlan) { + return { + type: 'plan', + mode: planResult.mode, + prompt: planResult.prompt, + originalMessage: message.trim() + }; + } return { type: null }; -} \ No newline at end of file +} diff --git a/web/README.md b/web/README.md index 383eee1f..4336d590 100644 --- a/web/README.md +++ b/web/README.md @@ -77,6 +77,7 @@ See `src/router.tsx` for route definitions. ### New session (`src/components/NewSession/`) Modular session creation: + - Machine selector - Directory input with recent paths - Agent type selector @@ -128,7 +129,9 @@ bun install bun run dev:web ``` + If testing in Telegram, set: + - `HAPI_PUBLIC_URL` to the public HTTPS URL of the dev server. - `CORS_ORIGINS` to include the dev server origin. diff --git a/web/package.json b/web/package.json index d3571527..1d7b9892 100644 --- a/web/package.json +++ b/web/package.json @@ -33,6 +33,7 @@ "diff": "^8.0.2", "hast-util-to-jsx-runtime": "^2.3.6", "katex": "^0.16.45", + "mermaid": "^11.12.0", "react": "^19.2.3", "react-dom": "^19.2.3", "rehype-katex": "^7.0.1", diff --git a/web/src/components/AssistantChat/HappyThread.test.tsx b/web/src/components/AssistantChat/HappyThread.test.tsx index 51be7fc5..45176b53 100644 --- a/web/src/components/AssistantChat/HappyThread.test.tsx +++ b/web/src/components/AssistantChat/HappyThread.test.tsx @@ -2,7 +2,13 @@ import { describe, expect, it, vi } from 'vitest' import { fireEvent, render, screen } from '@testing-library/react' import type { ComponentProps } from 'react' import { I18nProvider } from '@/lib/i18n-context' -import { ConversationOutlinePanel } from '@/components/AssistantChat/HappyThread' +import { + ConversationOutlinePanel, + captureScrollAnchor, + getScrollIntent, + restoreScrollAnchor, + shouldCancelInitialScrollSettling, +} from '@/components/AssistantChat/HappyThread' import type { ConversationOutlineItem } from '@/chat/outline' const outlineItems: ConversationOutlineItem[] = [ @@ -22,6 +28,19 @@ const outlineItems: ConversationOutlineItem[] = [ } ] +function rect(values: Pick & Partial): DOMRect { + return { + left: 0, + right: 300, + width: 300, + height: values.bottom - values.top, + x: 0, + y: values.top, + toJSON: () => ({}), + ...values + } as DOMRect +} + function renderPanel(props: Partial> = {}) { return render( @@ -64,3 +83,103 @@ describe('ConversationOutlinePanel', () => { expect(screen.getByText('No outline items in loaded messages')).toBeInTheDocument() }) }) + +describe('scroll anchor helpers', () => { + it('captures the first visible message relative to the viewport', () => { + const viewport = document.createElement('div') + const first = document.createElement('div') + const second = document.createElement('div') + first.id = 'first-message' + second.id = 'second-message' + viewport.className = 'viewport' + const messages = document.createElement('div') + messages.className = 'happy-thread-messages' + messages.append(first, second) + viewport.append(messages) + document.body.append(viewport) + + vi.spyOn(viewport, 'getBoundingClientRect').mockReturnValue(rect({ top: 100, bottom: 500 })) + vi.spyOn(first, 'getBoundingClientRect').mockReturnValue(rect({ top: 60, bottom: 90 })) + vi.spyOn(second, 'getBoundingClientRect').mockReturnValue(rect({ top: 120, bottom: 180 })) + + expect(captureScrollAnchor(viewport)).toEqual({ + id: 'second-message', + topOffset: 20 + }) + + viewport.remove() + }) + + it('treats upward motion near the bottom as manual scroll intent', () => { + expect(getScrollIntent({ + scrollTop: 690, + previousScrollTop: 702, + scrollHeight: 1232, + clientHeight: 530 + })).toMatchObject({ + distanceFromBottom: 12, + isNearBottom: true, + isScrollingUp: true + }) + }) + + it('does not classify downward movement as upward manual scroll intent', () => { + expect(getScrollIntent({ + scrollTop: 702, + previousScrollTop: 690, + scrollHeight: 1232, + clientHeight: 530 + })).toMatchObject({ + distanceFromBottom: 0, + isNearBottom: true, + isScrollingUp: false + }) + }) + + it('cancels initial scroll settling when the user scrolls up away from the bottom', () => { + const intent = getScrollIntent({ + scrollTop: 520, + previousScrollTop: 700, + scrollHeight: 1232, + clientHeight: 530 + }) + + expect(intent).toMatchObject({ + distanceFromBottom: 182, + isScrollingUp: true + }) + expect(shouldCancelInitialScrollSettling(intent)).toBe(true) + }) + + it('keeps initial scroll settling for negligible movement at the bottom', () => { + const intent = getScrollIntent({ + scrollTop: 702, + previousScrollTop: 702, + scrollHeight: 1232, + clientHeight: 530 + }) + + expect(intent).toMatchObject({ + distanceFromBottom: 0, + isScrollingUp: false + }) + expect(shouldCancelInitialScrollSettling(intent)).toBe(false) + }) + + it('restores the captured message to the same viewport offset', () => { + const viewport = document.createElement('div') + const message = document.createElement('div') + message.id = 'anchored-message' + viewport.append(message) + document.body.append(viewport) + viewport.scrollTop = 200 + + vi.spyOn(viewport, 'getBoundingClientRect').mockReturnValue(rect({ top: 100, bottom: 500 })) + vi.spyOn(message, 'getBoundingClientRect').mockReturnValue(rect({ top: 180, bottom: 260 })) + + expect(restoreScrollAnchor(viewport, { id: 'anchored-message', topOffset: 30 })).toBe(true) + expect(viewport.scrollTop).toBe(250) + + viewport.remove() + }) +}) diff --git a/web/src/components/AssistantChat/HappyThread.tsx b/web/src/components/AssistantChat/HappyThread.tsx index 6016fd43..986498b9 100644 --- a/web/src/components/AssistantChat/HappyThread.tsx +++ b/web/src/components/AssistantChat/HappyThread.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react' +import { useCallback, useEffect, useLayoutEffect, useRef } from 'react' import { ThreadPrimitive } from '@assistant-ui/react' import type { ApiClient } from '@/api/client' import type { SessionMetadataSummary } from '@/types/api' @@ -13,6 +13,75 @@ import { Spinner } from '@/components/Spinner' import { useTranslation } from '@/lib/use-translation' import { CloseIcon } from '@/components/icons' +type ScrollAnchor = { + id: string + topOffset: number +} + +type PendingScrollRestore = { + anchor: ScrollAnchor | null + scrollTop: number + scrollHeight: number +} + +const MESSAGE_ANCHOR_SELECTOR = '.happy-thread-messages > [id]' +const AUTO_SCROLL_RESUME_THRESHOLD_PX = 120 +const MANUAL_SCROLL_EPSILON_PX = 1 +const INITIAL_SCROLL_SETTLE_MS = 1800 +const INITIAL_SCROLL_SETTLE_DELAYS_MS = [0, 16, 50, 120, 250, 500, 900, 1400, 1800] as const + +type ScrollIntent = { + distanceFromBottom: number + isNearBottom: boolean + isScrollingUp: boolean +} + +export function getScrollIntent(params: { + scrollTop: number + scrollHeight: number + clientHeight: number + previousScrollTop: number + thresholdPx?: number +}): ScrollIntent { + const thresholdPx = params.thresholdPx ?? AUTO_SCROLL_RESUME_THRESHOLD_PX + const distanceFromBottom = params.scrollHeight - params.scrollTop - params.clientHeight + return { + distanceFromBottom, + isNearBottom: distanceFromBottom < thresholdPx, + isScrollingUp: params.scrollTop < params.previousScrollTop - MANUAL_SCROLL_EPSILON_PX + } +} + +export function shouldCancelInitialScrollSettling(intent: ScrollIntent): boolean { + return intent.isScrollingUp && intent.distanceFromBottom > MANUAL_SCROLL_EPSILON_PX +} + +export function captureScrollAnchor(viewport: HTMLElement): ScrollAnchor | null { + const viewportRect = viewport.getBoundingClientRect() + const messages = Array.from(viewport.querySelectorAll(MESSAGE_ANCHOR_SELECTOR)) + for (const message of messages) { + const rect = message.getBoundingClientRect() + if (rect.bottom > viewportRect.top && rect.top < viewportRect.bottom) { + return { + id: message.id, + topOffset: rect.top - viewportRect.top + } + } + } + return null +} + +export function restoreScrollAnchor(viewport: HTMLElement, anchor: ScrollAnchor): boolean { + const target = document.getElementById(anchor.id) + if (!target || !viewport.contains(target)) { + return false + } + const viewportRect = viewport.getBoundingClientRect() + const targetRect = target.getBoundingClientRect() + viewport.scrollTop += targetRect.top - viewportRect.top - anchor.topOffset + return true +} + function NewMessagesIndicator(props: { count: number; onClick: () => void }) { const { t } = useTranslation() if (props.count === 0) { @@ -176,9 +245,10 @@ export function HappyThread(props: { }) { const { t } = useTranslation() const viewportRef = useRef(null) + const contentRef = useRef(null) const topSentinelRef = useRef(null) const loadLockRef = useRef(false) - const pendingScrollRef = useRef<{ scrollTop: number; scrollHeight: number } | null>(null) + const pendingScrollRef = useRef(null) const prevLoadingMoreRef = useRef(false) const loadStartedRef = useRef(false) const isLoadingMoreRef = useRef(props.isLoadingMoreMessages) @@ -190,15 +260,14 @@ export function HappyThread(props: { const onAtBottomChangeRef = useRef(props.onAtBottomChange) const onFlushPendingRef = useRef(props.onFlushPending) const forceScrollTokenRef = useRef(props.forceScrollToken) + const lastScrollTopRef = useRef(0) + const sessionIdRef = useRef(props.sessionId) + const initialScrollSessionRef = useRef(null) + const initialScrollDeadlineRef = useRef(0) + const initialScrollTimersRef = useRef([]) - // Smart scroll state: autoScroll enabled when user is near bottom - const [autoScrollEnabled, setAutoScrollEnabled] = useState(true) - const autoScrollEnabledRef = useRef(autoScrollEnabled) - - // Keep refs in sync with state - useEffect(() => { - autoScrollEnabledRef.current = autoScrollEnabled - }, [autoScrollEnabled]) + // Smart scroll state: enabled only while the user is intentionally at the bottom. + const autoScrollEnabledRef = useRef(true) useEffect(() => { onAtBottomChangeRef.current = props.onAtBottomChange }, [props.onAtBottomChange]) @@ -215,43 +284,101 @@ export function HappyThread(props: { onLoadMoreRef.current = props.onLoadMore }, [props.onLoadMore]) + useEffect(() => { + sessionIdRef.current = props.sessionId + }, [props.sessionId]) + + const isInitialScrollSettling = useCallback(() => { + return initialScrollSessionRef.current === sessionIdRef.current && Date.now() < initialScrollDeadlineRef.current + }, []) + + const clearInitialScrollTimers = useCallback(() => { + for (const timer of initialScrollTimersRef.current) { + window.clearTimeout(timer) + } + initialScrollTimersRef.current = [] + }, []) + // Track scroll position to toggle autoScroll (stable listener using refs) useEffect(() => { const viewport = viewportRef.current if (!viewport) return - const THRESHOLD_PX = 120 + lastScrollTopRef.current = viewport.scrollTop + + const setAutoScrollMode = (enabled: boolean) => { + if (autoScrollEnabledRef.current === enabled) { + return + } + autoScrollEnabledRef.current = enabled + } + + const setAtBottomMode = (atBottom: boolean) => { + if (atBottom === atBottomRef.current) { + return + } + atBottomRef.current = atBottom + onAtBottomChangeRef.current(atBottom) + if (atBottom) { + onFlushPendingRef.current() + } + } const handleScroll = () => { - const distanceFromBottom = viewport.scrollHeight - viewport.scrollTop - viewport.clientHeight - const isNearBottom = distanceFromBottom < THRESHOLD_PX + const intent = getScrollIntent({ + scrollTop: viewport.scrollTop, + scrollHeight: viewport.scrollHeight, + clientHeight: viewport.clientHeight, + previousScrollTop: lastScrollTopRef.current + }) + lastScrollTopRef.current = viewport.scrollTop - if (isNearBottom) { - if (!autoScrollEnabledRef.current) setAutoScrollEnabled(true) - } else if (autoScrollEnabledRef.current) { - setAutoScrollEnabled(false) - } - - if (isNearBottom !== atBottomRef.current) { - atBottomRef.current = isNearBottom - onAtBottomChangeRef.current(isNearBottom) - if (isNearBottom) { - onFlushPendingRef.current() + if (isInitialScrollSettling()) { + if (shouldCancelInitialScrollSettling(intent)) { + initialScrollDeadlineRef.current = 0 + clearInitialScrollTimers() + setAutoScrollMode(false) + setAtBottomMode(false) } + return } + + if (intent.isScrollingUp && intent.distanceFromBottom > MANUAL_SCROLL_EPSILON_PX) { + setAutoScrollMode(false) + setAtBottomMode(false) + return + } + + if (intent.isNearBottom) { + setAutoScrollMode(true) + setAtBottomMode(true) + return + } + + setAutoScrollMode(false) + setAtBottomMode(false) } viewport.addEventListener('scroll', handleScroll, { passive: true }) return () => viewport.removeEventListener('scroll', handleScroll) }, []) // Stable: no dependencies, reads from refs + const scrollToBottomInstant = useCallback(() => { + const viewport = viewportRef.current + if (viewport) { + viewport.scrollTo({ top: viewport.scrollHeight, behavior: 'instant' }) + lastScrollTopRef.current = viewport.scrollTop + } + }, []) + // Scroll to bottom handler for the indicator button const scrollToBottom = useCallback(() => { const viewport = viewportRef.current if (viewport) { viewport.scrollTo({ top: viewport.scrollHeight, behavior: 'smooth' }) + lastScrollTopRef.current = viewport.scrollTop } - setAutoScrollEnabled(true) + autoScrollEnabledRef.current = true if (!atBottomRef.current) { atBottomRef.current = true onAtBottomChangeRef.current(true) @@ -260,12 +387,59 @@ export function HappyThread(props: { }, []) // Reset state when session changes - useEffect(() => { - setAutoScrollEnabled(true) + useLayoutEffect(() => { + autoScrollEnabledRef.current = true + lastScrollTopRef.current = viewportRef.current?.scrollTop ?? 0 atBottomRef.current = true onAtBottomChangeRef.current(true) forceScrollTokenRef.current = props.forceScrollToken - }, [props.sessionId]) + initialScrollSessionRef.current = null + initialScrollDeadlineRef.current = 0 + clearInitialScrollTimers() + }, [props.sessionId, clearInitialScrollTimers]) + + useLayoutEffect(() => { + if ( + initialScrollSessionRef.current === props.sessionId + || props.isLoadingMessages + || props.rawMessagesCount === 0 + || pendingScrollRef.current + ) { + return + } + + initialScrollSessionRef.current = props.sessionId + autoScrollEnabledRef.current = true + atBottomRef.current = true + onAtBottomChangeRef.current(true) + scrollToBottomInstant() + + initialScrollDeadlineRef.current = Date.now() + INITIAL_SCROLL_SETTLE_MS + clearInitialScrollTimers() + initialScrollTimersRef.current = INITIAL_SCROLL_SETTLE_DELAYS_MS.map((delay) => window.setTimeout(() => { + if ( + initialScrollSessionRef.current !== props.sessionId + || !autoScrollEnabledRef.current + || pendingScrollRef.current + ) { + return + } + scrollToBottomInstant() + }, delay)) + }, [ + props.sessionId, + props.isLoadingMessages, + props.rawMessagesCount, + props.messagesVersion, + scrollToBottomInstant, + clearInitialScrollTimers + ]) + + useEffect(() => { + return () => { + clearInitialScrollTimers() + } + }, [clearInitialScrollTimers]) useEffect(() => { if (forceScrollTokenRef.current === props.forceScrollToken) { @@ -276,7 +450,13 @@ export function HappyThread(props: { }, [props.forceScrollToken, scrollToBottom]) const handleLoadMore = useCallback(() => { - if (isLoadingMessagesRef.current || !hasMoreMessagesRef.current || isLoadingMoreRef.current || loadLockRef.current) { + if ( + isInitialScrollSettling() + || isLoadingMessagesRef.current + || !hasMoreMessagesRef.current + || isLoadingMoreRef.current + || loadLockRef.current + ) { return } const viewport = viewportRef.current @@ -284,9 +464,11 @@ export function HappyThread(props: { return } pendingScrollRef.current = { + anchor: captureScrollAnchor(viewport), scrollTop: viewport.scrollTop, scrollHeight: viewport.scrollHeight } + autoScrollEnabledRef.current = false loadLockRef.current = true loadStartedRef.current = false let loadPromise: Promise @@ -307,13 +489,13 @@ export function HappyThread(props: { loadLockRef.current = false } }) - }, []) + }, [isInitialScrollSettling]) const handleOutlineSelect = useCallback((item: ConversationOutlineItem) => { const target = document.getElementById(getConversationMessageAnchorId(item.targetMessageId)) if (target) { target.scrollIntoView({ block: 'start', behavior: 'smooth' }) - setAutoScrollEnabled(false) + autoScrollEnabledRef.current = false } props.onOutlineItemClick?.(item) props.onOutlineOpenChange(false) @@ -337,6 +519,9 @@ export function HappyThread(props: { (entries) => { for (const entry of entries) { if (entry.isIntersecting) { + if (isInitialScrollSettling()) { + continue + } handleLoadMoreRef.current() } } @@ -349,19 +534,44 @@ export function HappyThread(props: { observer.observe(sentinel) return () => observer.disconnect() - }, [props.hasMoreMessages, props.isLoadingMessages]) + }, [props.hasMoreMessages, props.isLoadingMessages, isInitialScrollSettling]) + + useEffect(() => { + const content = contentRef.current + if (!content || typeof ResizeObserver === 'undefined') { + return + } + + const observer = new ResizeObserver(() => { + if (isInitialScrollSettling() && autoScrollEnabledRef.current && !pendingScrollRef.current) { + scrollToBottomInstant() + } + }) + observer.observe(content) + return () => observer.disconnect() + }, [isInitialScrollSettling, scrollToBottomInstant]) useLayoutEffect(() => { const pending = pendingScrollRef.current const viewport = viewportRef.current - if (!pending || !viewport) { + if (!viewport) { return } - const delta = viewport.scrollHeight - pending.scrollHeight - viewport.scrollTop = pending.scrollTop + delta - pendingScrollRef.current = null - loadLockRef.current = false - }, [props.messagesVersion]) + if (pending) { + const restoredByAnchor = pending.anchor ? restoreScrollAnchor(viewport, pending.anchor) : false + if (!restoredByAnchor) { + const delta = viewport.scrollHeight - pending.scrollHeight + viewport.scrollTop = pending.scrollTop + delta + } + lastScrollTopRef.current = viewport.scrollTop + pendingScrollRef.current = null + loadLockRef.current = false + return + } + if (atBottomRef.current && autoScrollEnabledRef.current) { + scrollToBottomInstant() + } + }, [props.messagesVersion, scrollToBottomInstant]) useEffect(() => { isLoadingMoreRef.current = props.isLoadingMoreMessages @@ -387,9 +597,15 @@ export function HappyThread(props: { onRetryMessage: props.onRetryMessage }}> - +
-
+