diff --git a/site/package.json b/site/package.json index 0a2bc26baf..cea9343741 100644 --- a/site/package.json +++ b/site/package.json @@ -46,6 +46,8 @@ "@fontsource/ibm-plex-mono": "5.2.7", "@fontsource/jetbrains-mono": "5.2.8", "@fontsource/source-code-pro": "5.2.7", + "@lexical/react": "0.41.0", + "@lexical/utils": "0.41.0", "@monaco-editor/react": "4.7.0", "@mui/material": "5.18.0", "@mui/system": "5.18.0", @@ -90,6 +92,7 @@ "front-matter": "4.0.2", "humanize-duration": "3.33.1", "jszip": "3.10.1", + "lexical": "0.41.0", "lodash": "4.17.21", "lucide-react": "0.555.0", "monaco-editor": "0.55.1", diff --git a/site/pnpm-lock.yaml b/site/pnpm-lock.yaml index 2c075a6e31..1757d1ba07 100644 --- a/site/pnpm-lock.yaml +++ b/site/pnpm-lock.yaml @@ -52,6 +52,12 @@ importers: '@fontsource/source-code-pro': specifier: 5.2.7 version: 5.2.7 + '@lexical/react': + specifier: 0.41.0 + version: 0.41.0(react-dom@19.2.2(react@19.2.2))(react@19.2.2)(yjs@13.6.29) + '@lexical/utils': + specifier: 0.41.0 + version: 0.41.0 '@monaco-editor/react': specifier: 4.7.0 version: 4.7.0(monaco-editor@0.55.1)(react-dom@19.2.2(react@19.2.2))(react@19.2.2) @@ -184,6 +190,9 @@ importers: jszip: specifier: 3.10.1 version: 3.10.1 + lexical: + specifier: 0.41.0 + version: 0.41.0 lodash: specifier: 4.17.21 version: 4.17.21 @@ -1037,15 +1046,33 @@ packages: '@floating-ui/core@1.7.3': resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==, tarball: https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz} + '@floating-ui/core@1.7.4': + resolution: {integrity: sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==, tarball: https://registry.npmjs.org/@floating-ui/core/-/core-1.7.4.tgz} + '@floating-ui/dom@1.7.4': resolution: {integrity: sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==, tarball: https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz} + '@floating-ui/dom@1.7.5': + resolution: {integrity: sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==, tarball: https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.5.tgz} + '@floating-ui/react-dom@2.1.6': resolution: {integrity: sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==, tarball: https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz} peerDependencies: react: '>=16.8.0' react-dom: '>=16.8.0' + '@floating-ui/react-dom@2.1.7': + resolution: {integrity: sha512-0tLRojf/1Go2JgEVm+3Frg9A3IW8bJgKgdO0BN5RkF//ufuz2joZM63Npau2ff3J6lUVYgDSNzNkR+aH3IVfjg==, tarball: https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.7.tgz} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/react@0.27.18': + resolution: {integrity: sha512-xJWJxvmy3a05j643gQt+pRbht5XnTlGpsEsAPnMi5F5YTOEEJymA90uZKBD8OvIv5XvZ1qi4GcccSlqT3Bq44Q==, tarball: https://registry.npmjs.org/@floating-ui/react/-/react-0.27.18.tgz} + peerDependencies: + react: '>=17.0.0' + react-dom: '>=17.0.0' + '@floating-ui/utils@0.2.10': resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==, tarball: https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz} @@ -1233,6 +1260,80 @@ packages: '@leeoniya/ufuzzy@1.0.10': resolution: {integrity: sha512-OR1yiyN8cKBn5UiHjKHUl0LcrTQt4vZPUpIf96qIIZVLxgd4xyASuRvTZ3tjbWvuyQAMgvKsq61Nwu131YyHnA==, tarball: https://registry.npmjs.org/@leeoniya/ufuzzy/-/ufuzzy-1.0.10.tgz} + '@lexical/clipboard@0.41.0': + resolution: {integrity: sha512-Ex5lPkb4NBBX1DCPzOAIeHBJFH1bJcmATjREaqpnTfxCbuOeQkt44wchezUA0oDl+iAxNZ3+pLLWiUju9icoSA==, tarball: https://registry.npmjs.org/@lexical/clipboard/-/clipboard-0.41.0.tgz} + + '@lexical/code@0.41.0': + resolution: {integrity: sha512-0hoNi1KC9/N3SBOGcOcFqnT0OpwmcRRAhfxTKMGqfCtCvAMzULVwZ8RWc9/NV9bKYESgBTW5D9xkDANP2mspHg==, tarball: https://registry.npmjs.org/@lexical/code/-/code-0.41.0.tgz} + + '@lexical/devtools-core@0.41.0': + resolution: {integrity: sha512-FzJtluBhBc8bKS11TUZe72KoZN/hnzIyiiM0SPJAsPwGpoXuM01jqpXQGybWf/1bWB+bmmhOae7O4Nywi/Csuw==, tarball: https://registry.npmjs.org/@lexical/devtools-core/-/devtools-core-0.41.0.tgz} + peerDependencies: + react: '>=17.x' + react-dom: '>=17.x' + + '@lexical/dragon@0.41.0': + resolution: {integrity: sha512-gBEqkk8Q6ZPruvDaRcOdF1EK9suCVBODzOCcR+EnoJTaTjfDkCM7pkPAm4w90Wa1wCZEtFHvCfas+jU9MDSumg==, tarball: https://registry.npmjs.org/@lexical/dragon/-/dragon-0.41.0.tgz} + + '@lexical/extension@0.41.0': + resolution: {integrity: sha512-sF4SPiP72yXvIGchmmIZ7Yg2XZTxNLOpFEIIzdqG7X/1fa1Ham9P/T7VbrblWpF6Ei5LJtK9JgNVB0hb4l3o1g==, tarball: https://registry.npmjs.org/@lexical/extension/-/extension-0.41.0.tgz} + + '@lexical/hashtag@0.41.0': + resolution: {integrity: sha512-tFWM74RW4KU0E/sj2aowfWl26vmLUTp331CgVESnhQKcZBfT40KJYd57HEqBDTfQKn4MUhylQCCA0hbpw6EeFQ==, tarball: https://registry.npmjs.org/@lexical/hashtag/-/hashtag-0.41.0.tgz} + + '@lexical/history@0.41.0': + resolution: {integrity: sha512-kGoVWsiOn62+RMjRolRa+NXZl8jFwxav6GNDiHH8yzivtoaH8n1SwUfLJELXCzeqzs81HySqD4q30VLJVTGoDg==, tarball: https://registry.npmjs.org/@lexical/history/-/history-0.41.0.tgz} + + '@lexical/html@0.41.0': + resolution: {integrity: sha512-3RyZy+H/IDKz2D66rNN/NqYx87xVFrngfEbyu1OWtbY963RUFnopiVHCQvsge/8kT04QSZ7U/DzjVFqeNS6clg==, tarball: https://registry.npmjs.org/@lexical/html/-/html-0.41.0.tgz} + + '@lexical/link@0.41.0': + resolution: {integrity: sha512-Rjtx5cGWAkKcnacncbVsZ1TqRnUB2Wm4eEVKpaAEG41+kHgqghzM2P+UGT15yROroxJu8KvAC9ISiYFiU4XE1w==, tarball: https://registry.npmjs.org/@lexical/link/-/link-0.41.0.tgz} + + '@lexical/list@0.41.0': + resolution: {integrity: sha512-RXvB+xcbzVoQLGRDOBRCacztG7V+bI95tdoTwl8pz5xvgPtAaRnkZWMDP+yMNzMJZsqEChdtpxbf0NgtMkun6g==, tarball: https://registry.npmjs.org/@lexical/list/-/list-0.41.0.tgz} + + '@lexical/mark@0.41.0': + resolution: {integrity: sha512-UO5WVs9uJAYIKHSlYh4Z1gHrBBchTOi21UCYBIZ7eAs4suK84hPzD+3/LAX5CB7ZltL6ke5Sly3FOwNXv/wfpA==, tarball: https://registry.npmjs.org/@lexical/mark/-/mark-0.41.0.tgz} + + '@lexical/markdown@0.41.0': + resolution: {integrity: sha512-bzI73JMXpjGFhqUWNV6KqfjWcgAWzwFT+J3RHtbCF5rysC8HLldBYojOgAAtPfXqfxyv2mDzsY7SoJ75s9uHZA==, tarball: https://registry.npmjs.org/@lexical/markdown/-/markdown-0.41.0.tgz} + + '@lexical/offset@0.41.0': + resolution: {integrity: sha512-2RHBXZqC8gm3X9C0AyRb0M8w7zJu5dKiasrif+jSKzsxPjAUeF1m95OtIOsWs1XLNUgASOSUqGovDZxKJslZfA==, tarball: https://registry.npmjs.org/@lexical/offset/-/offset-0.41.0.tgz} + + '@lexical/overflow@0.41.0': + resolution: {integrity: sha512-Iy6ZiJip8X14EBYt1zKPOrXyQ4eG9JLBEoPoSVBTiSbVd+lYicdUvaOThT0k0/qeVTN9nqTaEltBjm56IrVKCQ==, tarball: https://registry.npmjs.org/@lexical/overflow/-/overflow-0.41.0.tgz} + + '@lexical/plain-text@0.41.0': + resolution: {integrity: sha512-HIsGgmFUYRUNNyvckun33UQfU7LRzDlxymHUq67+Bxd5bXqdZOrStEKJXuDX+LuLh/GXZbaWNbDLqwLBObfbQg==, tarball: https://registry.npmjs.org/@lexical/plain-text/-/plain-text-0.41.0.tgz} + + '@lexical/react@0.41.0': + resolution: {integrity: sha512-7+GUdZUm6sofWm+zdsWAs6cFBwKNsvsHezZTrf6k8jrZxL461ZQmbz/16b4DvjCGL9r5P1fR7md9/LCmk8TiCg==, tarball: https://registry.npmjs.org/@lexical/react/-/react-0.41.0.tgz} + peerDependencies: + react: '>=17.x' + react-dom: '>=17.x' + + '@lexical/rich-text@0.41.0': + resolution: {integrity: sha512-yUcr7ZaaVTZNi8bow4CK1M8jy2qyyls1Vr+5dVjwBclVShOL/F/nFyzBOSb6RtXXRbd3Ahuk9fEleppX/RNIdw==, tarball: https://registry.npmjs.org/@lexical/rich-text/-/rich-text-0.41.0.tgz} + + '@lexical/selection@0.41.0': + resolution: {integrity: sha512-1s7/kNyRzcv5uaTwsUL28NpiisqTf5xZ1zNukLsCN1xY+TWbv9RE9OxIv+748wMm4pxNczQe/UbIBODkbeknLw==, tarball: https://registry.npmjs.org/@lexical/selection/-/selection-0.41.0.tgz} + + '@lexical/table@0.41.0': + resolution: {integrity: sha512-d3SPThBAr+oZ8O74TXU0iXM3rLbrAVC7/HcOnSAq7/AhWQW8yMutT51JQGN+0fMLP9kqoWSAojNtkdvzXfU/+A==, tarball: https://registry.npmjs.org/@lexical/table/-/table-0.41.0.tgz} + + '@lexical/text@0.41.0': + resolution: {integrity: sha512-gGA+Anc7ck110EXo4KVKtq6Ui3M7Vz3OpGJ4QE6zJHWW8nV5h273koUGSutAMeoZgRVb6t01Izh3ORoFt/j1CA==, tarball: https://registry.npmjs.org/@lexical/text/-/text-0.41.0.tgz} + + '@lexical/utils@0.41.0': + resolution: {integrity: sha512-Wlsokr5NQCq83D+7kxZ9qs5yQ3dU3Qaf2M+uXxLRoPoDaXqW8xTWZq1+ZFoEzsHzx06QoPa4Vu/40BZR91uQPg==, tarball: https://registry.npmjs.org/@lexical/utils/-/utils-0.41.0.tgz} + + '@lexical/yjs@0.41.0': + resolution: {integrity: sha512-PaKTxSbVC4fpqUjQ7vUL9RkNF1PjL8TFl5jRe03PqoPYpE33buf3VXX6+cOUEfv9+uknSqLCPHoBS/4jN3a97w==, tarball: https://registry.npmjs.org/@lexical/yjs/-/yjs-0.41.0.tgz} + peerDependencies: + yjs: '>=13.5.22' + '@mdx-js/react@3.1.1': resolution: {integrity: sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==, tarball: https://registry.npmjs.org/@mdx-js/react/-/react-3.1.1.tgz} peerDependencies: @@ -1507,6 +1608,9 @@ packages: '@popperjs/core@2.11.8': resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==, tarball: https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz} + '@preact/signals-core@1.13.0': + resolution: {integrity: sha512-slT6XeTCAbdql61GVLlGU4x7XHI7kCZV5Um5uhE4zLX4ApgiiXc0UYFvVOKq06xcovzp7p+61l68oPi563ARKg==, tarball: https://registry.npmjs.org/@preact/signals-core/-/signals-core-1.13.0.tgz} + '@protobufjs/aspromise@1.1.2': resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==, tarball: https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz} @@ -4266,6 +4370,9 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==, tarball: https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz} + isomorphic.js@0.2.5: + resolution: {integrity: sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==, tarball: https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz} + istanbul-lib-coverage@3.2.2: resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==, tarball: https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz} engines: {node: '>=8'} @@ -4568,6 +4675,14 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==, tarball: https://registry.npmjs.org/levn/-/levn-0.4.1.tgz} engines: {node: '>= 0.8.0'} + lexical@0.41.0: + resolution: {integrity: sha512-pNIm5+n+hVnJHB9gYPDYsIO5Y59dNaDU9rJmPPsfqQhP2ojKFnUoPbcRnrI9FJLXB14sSumcY8LUw7Sq70TZqA==, tarball: https://registry.npmjs.org/lexical/-/lexical-0.41.0.tgz} + + lib0@0.2.117: + resolution: {integrity: sha512-DeXj9X5xDCjgKLU/7RR+/HQEVzuuEUiwldwOGsHK/sfAfELGWEyTcf0x+uOvCvK3O2zPmZePXWL85vtia6GyZw==, tarball: https://registry.npmjs.org/lib0/-/lib0-0.2.117.tgz} + engines: {node: '>=16'} + hasBin: true + lie@3.3.0: resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==, tarball: https://registry.npmjs.org/lie/-/lie-3.3.0.tgz} @@ -5329,6 +5444,11 @@ packages: peerDependencies: react: ^19.2.2 + react-error-boundary@6.1.1: + resolution: {integrity: sha512-BrYwPOdXi5mqkk5lw+Uvt0ThHx32rCt3BkukS4X23A2AIWDPSGX6iaWTc0y9TU/mHDA/6qOSGel+B2ERkOvD1w==, tarball: https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-6.1.1.tgz} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-fast-compare@2.0.4: resolution: {integrity: sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==, tarball: https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz} @@ -5891,6 +6011,9 @@ packages: symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==, tarball: https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz} + tabbable@6.4.0: + resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==, tarball: https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz} + tailwind-merge@2.6.0: resolution: {integrity: sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==, tarball: https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.0.tgz} @@ -6499,6 +6622,10 @@ packages: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==, tarball: https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz} engines: {node: '>=12'} + yjs@13.6.29: + resolution: {integrity: sha512-kHqDPdltoXH+X4w1lVmMtddE3Oeqq48nM40FD5ojTd8xYhQpzIDcfE2keMSU5bAgRPJBe225WTUdyUgj1DtbiQ==, tarball: https://registry.npmjs.org/yjs/-/yjs-13.6.29.tgz} + engines: {node: '>=16.0.0', npm: '>=8.0.0'} + yn@3.1.1: resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==, tarball: https://registry.npmjs.org/yn/-/yn-3.1.1.tgz} engines: {node: '>=6'} @@ -7066,17 +7193,40 @@ snapshots: dependencies: '@floating-ui/utils': 0.2.10 + '@floating-ui/core@1.7.4': + dependencies: + '@floating-ui/utils': 0.2.10 + '@floating-ui/dom@1.7.4': dependencies: '@floating-ui/core': 1.7.3 '@floating-ui/utils': 0.2.10 + '@floating-ui/dom@1.7.5': + dependencies: + '@floating-ui/core': 1.7.4 + '@floating-ui/utils': 0.2.10 + '@floating-ui/react-dom@2.1.6(react-dom@19.2.2(react@19.2.2))(react@19.2.2)': dependencies: '@floating-ui/dom': 1.7.4 react: 19.2.2 react-dom: 19.2.2(react@19.2.2) + '@floating-ui/react-dom@2.1.7(react-dom@19.2.2(react@19.2.2))(react@19.2.2)': + dependencies: + '@floating-ui/dom': 1.7.5 + react: 19.2.2 + react-dom: 19.2.2(react@19.2.2) + + '@floating-ui/react@0.27.18(react-dom@19.2.2(react@19.2.2))(react@19.2.2)': + dependencies: + '@floating-ui/react-dom': 2.1.7(react-dom@19.2.2(react@19.2.2))(react@19.2.2) + '@floating-ui/utils': 0.2.10 + react: 19.2.2 + react-dom: 19.2.2(react@19.2.2) + tabbable: 6.4.0 + '@floating-ui/utils@0.2.10': {} '@fontsource-variable/geist@5.2.8': {} @@ -7390,6 +7540,165 @@ snapshots: '@leeoniya/ufuzzy@1.0.10': {} + '@lexical/clipboard@0.41.0': + dependencies: + '@lexical/html': 0.41.0 + '@lexical/list': 0.41.0 + '@lexical/selection': 0.41.0 + '@lexical/utils': 0.41.0 + lexical: 0.41.0 + + '@lexical/code@0.41.0': + dependencies: + '@lexical/utils': 0.41.0 + lexical: 0.41.0 + prismjs: 1.30.0 + + '@lexical/devtools-core@0.41.0(react-dom@19.2.2(react@19.2.2))(react@19.2.2)': + dependencies: + '@lexical/html': 0.41.0 + '@lexical/link': 0.41.0 + '@lexical/mark': 0.41.0 + '@lexical/table': 0.41.0 + '@lexical/utils': 0.41.0 + lexical: 0.41.0 + react: 19.2.2 + react-dom: 19.2.2(react@19.2.2) + + '@lexical/dragon@0.41.0': + dependencies: + '@lexical/extension': 0.41.0 + lexical: 0.41.0 + + '@lexical/extension@0.41.0': + dependencies: + '@lexical/utils': 0.41.0 + '@preact/signals-core': 1.13.0 + lexical: 0.41.0 + + '@lexical/hashtag@0.41.0': + dependencies: + '@lexical/text': 0.41.0 + '@lexical/utils': 0.41.0 + lexical: 0.41.0 + + '@lexical/history@0.41.0': + dependencies: + '@lexical/extension': 0.41.0 + '@lexical/utils': 0.41.0 + lexical: 0.41.0 + + '@lexical/html@0.41.0': + dependencies: + '@lexical/selection': 0.41.0 + '@lexical/utils': 0.41.0 + lexical: 0.41.0 + + '@lexical/link@0.41.0': + dependencies: + '@lexical/extension': 0.41.0 + '@lexical/utils': 0.41.0 + lexical: 0.41.0 + + '@lexical/list@0.41.0': + dependencies: + '@lexical/extension': 0.41.0 + '@lexical/selection': 0.41.0 + '@lexical/utils': 0.41.0 + lexical: 0.41.0 + + '@lexical/mark@0.41.0': + dependencies: + '@lexical/utils': 0.41.0 + lexical: 0.41.0 + + '@lexical/markdown@0.41.0': + dependencies: + '@lexical/code': 0.41.0 + '@lexical/link': 0.41.0 + '@lexical/list': 0.41.0 + '@lexical/rich-text': 0.41.0 + '@lexical/text': 0.41.0 + '@lexical/utils': 0.41.0 + lexical: 0.41.0 + + '@lexical/offset@0.41.0': + dependencies: + lexical: 0.41.0 + + '@lexical/overflow@0.41.0': + dependencies: + lexical: 0.41.0 + + '@lexical/plain-text@0.41.0': + dependencies: + '@lexical/clipboard': 0.41.0 + '@lexical/dragon': 0.41.0 + '@lexical/selection': 0.41.0 + '@lexical/utils': 0.41.0 + lexical: 0.41.0 + + '@lexical/react@0.41.0(react-dom@19.2.2(react@19.2.2))(react@19.2.2)(yjs@13.6.29)': + dependencies: + '@floating-ui/react': 0.27.18(react-dom@19.2.2(react@19.2.2))(react@19.2.2) + '@lexical/devtools-core': 0.41.0(react-dom@19.2.2(react@19.2.2))(react@19.2.2) + '@lexical/dragon': 0.41.0 + '@lexical/extension': 0.41.0 + '@lexical/hashtag': 0.41.0 + '@lexical/history': 0.41.0 + '@lexical/link': 0.41.0 + '@lexical/list': 0.41.0 + '@lexical/mark': 0.41.0 + '@lexical/markdown': 0.41.0 + '@lexical/overflow': 0.41.0 + '@lexical/plain-text': 0.41.0 + '@lexical/rich-text': 0.41.0 + '@lexical/table': 0.41.0 + '@lexical/text': 0.41.0 + '@lexical/utils': 0.41.0 + '@lexical/yjs': 0.41.0(yjs@13.6.29) + lexical: 0.41.0 + react: 19.2.2 + react-dom: 19.2.2(react@19.2.2) + react-error-boundary: 6.1.1(react@19.2.2) + transitivePeerDependencies: + - yjs + + '@lexical/rich-text@0.41.0': + dependencies: + '@lexical/clipboard': 0.41.0 + '@lexical/dragon': 0.41.0 + '@lexical/selection': 0.41.0 + '@lexical/utils': 0.41.0 + lexical: 0.41.0 + + '@lexical/selection@0.41.0': + dependencies: + lexical: 0.41.0 + + '@lexical/table@0.41.0': + dependencies: + '@lexical/clipboard': 0.41.0 + '@lexical/extension': 0.41.0 + '@lexical/utils': 0.41.0 + lexical: 0.41.0 + + '@lexical/text@0.41.0': + dependencies: + lexical: 0.41.0 + + '@lexical/utils@0.41.0': + dependencies: + '@lexical/selection': 0.41.0 + lexical: 0.41.0 + + '@lexical/yjs@0.41.0(yjs@13.6.29)': + dependencies: + '@lexical/offset': 0.41.0 + '@lexical/selection': 0.41.0 + lexical: 0.41.0 + yjs: 13.6.29 + '@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.2)': dependencies: '@types/mdx': 2.0.13 @@ -7645,6 +7954,8 @@ snapshots: '@popperjs/core@2.11.8': {} + '@preact/signals-core@1.13.0': {} + '@protobufjs/aspromise@1.1.2': {} '@protobufjs/base64@1.1.2': {} @@ -10555,6 +10866,8 @@ snapshots: isexe@2.0.0: {} + isomorphic.js@0.2.5: {} + istanbul-lib-coverage@3.2.2: {} istanbul-lib-instrument@5.2.1: @@ -11128,6 +11441,12 @@ snapshots: type-check: 0.4.0 optional: true + lexical@0.41.0: {} + + lib0@0.2.117: + dependencies: + isomorphic.js: 0.2.5 + lie@3.3.0: dependencies: immediate: 3.0.6 @@ -12094,6 +12413,10 @@ snapshots: react: 19.2.2 scheduler: 0.27.0 + react-error-boundary@6.1.1(react@19.2.2): + dependencies: + react: 19.2.2 + react-fast-compare@2.0.4: {} react-inspector@6.0.2(react@19.2.2): @@ -12762,6 +13085,8 @@ snapshots: symbol-tree@3.2.4: {} + tabbable@6.4.0: {} + tailwind-merge@2.6.0: {} tailwind-merge@3.4.1: {} @@ -13330,6 +13655,10 @@ snapshots: y18n: 5.0.8 yargs-parser: 21.1.1 + yjs@13.6.29: + dependencies: + lib0: 0.2.117 + yn@3.1.1: optional: true diff --git a/site/src/components/ChatMessageInput/ChatMessageInput.tsx b/site/src/components/ChatMessageInput/ChatMessageInput.tsx new file mode 100644 index 0000000000..d34f1470c2 --- /dev/null +++ b/site/src/components/ChatMessageInput/ChatMessageInput.tsx @@ -0,0 +1,391 @@ +import { AutoFocusPlugin } from "@lexical/react/LexicalAutoFocusPlugin"; +import { LexicalComposer } from "@lexical/react/LexicalComposer"; +import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; +import { ContentEditable } from "@lexical/react/LexicalContentEditable"; +import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary"; +import { HistoryPlugin } from "@lexical/react/LexicalHistoryPlugin"; +import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin"; +import { mergeRegister } from "@lexical/utils"; +import { + $createParagraphNode, + $createTextNode, + $getRoot, + $getSelection, + $insertNodes, + $isRangeSelection, + COMMAND_PRIORITY_HIGH, + FORMAT_ELEMENT_COMMAND, + FORMAT_TEXT_COMMAND, + KEY_ENTER_COMMAND, + type LexicalEditor, + PASTE_COMMAND, + type ParagraphNode, +} from "lexical"; +import { + type FC, + memo, + useCallback, + useEffect, + useImperativeHandle, + useMemo, + useRef, +} from "react"; +import { cn } from "utils/cn"; + +// Blocks Cmd+B/I/U and element formatting shortcuts so the editor +// stays plain-text only. +const DisableFormattingPlugin: FC = memo(function DisableFormattingPlugin() { + const [editor] = useLexicalComposerContext(); + + useEffect(() => { + return mergeRegister( + editor.registerCommand( + FORMAT_TEXT_COMMAND, + () => true, + COMMAND_PRIORITY_HIGH, + ), + editor.registerCommand( + FORMAT_ELEMENT_COMMAND, + () => true, + COMMAND_PRIORITY_HIGH, + ), + ); + }, [editor]); + + return null; +}); + +// Intercepts paste events and inserts clipboard content as plain text, +// stripping any rich-text formatting. +const PasteSanitizationPlugin: FC = memo(function PasteSanitizationPlugin() { + const [editor] = useLexicalComposerContext(); + + useEffect(() => { + return editor.registerCommand( + PASTE_COMMAND, + (event: ClipboardEvent | null) => { + if (!event) return false; + const clipboardData = event.clipboardData; + if (!clipboardData) return false; + + const text = clipboardData.getData("text/plain"); + if (!text) return false; + + event.preventDefault(); + + editor.update(() => { + const selection = $getSelection(); + if ($isRangeSelection(selection)) { + selection.insertText(text); + } else { + const root = $getRoot(); + const lastChild = root.getLastChild(); + if (lastChild) { + if (lastChild.getType() === "paragraph") { + const paragraph = lastChild as ParagraphNode; + const textNode = $createTextNode(text); + paragraph.append(textNode); + textNode.selectEnd(); + } else { + const textNode = $createTextNode(text); + lastChild.insertAfter(textNode); + textNode.selectEnd(); + } + } else { + const paragraph = $createParagraphNode(); + const textNode = $createTextNode(text); + paragraph.append(textNode); + root.append(paragraph); + textNode.selectEnd(); + } + } + }); + + return true; + }, + COMMAND_PRIORITY_HIGH, + ); + }, [editor]); + + return null; +}); + +// Handles Enter key behavior: plain Enter submits via the onEnter +// callback, Shift+Enter inserts a newline. +const EnterKeyPlugin: FC<{ onEnter?: () => void }> = memo( + function EnterKeyPlugin({ onEnter }) { + const [editor] = useLexicalComposerContext(); + + useEffect(() => { + return editor.registerCommand( + KEY_ENTER_COMMAND, + (event: KeyboardEvent | null) => { + if (event?.shiftKey) { + return false; + } + if (onEnter) { + event?.preventDefault(); + onEnter(); + } + return true; + }, + COMMAND_PRIORITY_HIGH, + ); + }, [editor, onEnter]); + + return null; + }, +); + +// Fires the onChange callback with the editor's plain-text content +// on every update. +const ContentChangePlugin: FC<{ + onChange?: (content: string) => void; +}> = memo(function ContentChangePlugin({ onChange }) { + const [editor] = useLexicalComposerContext(); + + useEffect(() => { + if (!onChange) return; + + return editor.registerUpdateListener(({ editorState }) => { + editorState.read(() => { + const root = $getRoot(); + const content = root.getTextContent(); + onChange(content); + }); + }); + }, [editor, onChange]); + + return null; +}); + +// Seeds the editor with an initial value on first mount. +const ValueSyncPlugin: FC<{ initialValue?: string }> = memo( + function ValueSyncPlugin({ initialValue }) { + const [editor] = useLexicalComposerContext(); + const hasInitialized = useRef(false); + + useEffect(() => { + if (!hasInitialized.current && initialValue !== undefined) { + hasInitialized.current = true; + + if (initialValue === "") { + return; + } + + editor.update(() => { + const root = $getRoot(); + root.clear(); + const paragraph = $createParagraphNode(); + const textNode = $createTextNode(initialValue); + paragraph.append(textNode); + root.append(paragraph); + }); + } + }, [editor, initialValue]); + + return null; + }, +); + +// Exposes the LexicalEditor instance to the parent via a callback +// so it can be stored in a ref for imperative access. +const InsertTextPlugin: FC<{ + onEditorReady: (editor: LexicalEditor) => void; +}> = memo(function InsertTextPlugin({ onEditorReady }) { + const [editor] = useLexicalComposerContext(); + + useEffect(() => { + onEditorReady(editor); + }, [editor, onEditorReady]); + + return null; +}); + +export interface ChatMessageInputRef { + insertText: (text: string) => void; + clear: () => void; + focus: () => void; + getValue: () => string; +} + +interface ChatMessageInputProps + extends Omit, "onChange" | "role" | "ref"> { + placeholder?: string; + initialValue?: string; + onChange?: (content: string) => void; + rows?: number; + onEnter?: () => void; + disabled?: boolean; + autoFocus?: boolean; + "aria-label"?: string; +} + +const ChatMessageInput = memo( + ({ + className, + placeholder, + initialValue, + onChange, + rows, + onEnter, + disabled, + autoFocus, + "aria-label": ariaLabel, + ref, + ...props + }: ChatMessageInputProps & { ref?: React.Ref }) => { + const initialConfig = useMemo( + () => ({ + namespace: "ChatMessageInput", + theme: { + paragraph: "m-0", + }, + onError: (error: Error) => console.error("Lexical error:", error), + nodes: [], + }), + [], + ); + const style = useMemo( + () => ({ + minHeight: rows ? `${rows * 1.5}rem` : undefined, + }), + [rows], + ); + + const editorRef = useRef(null); + + const handleEditorReady = useCallback((editor: LexicalEditor) => { + editorRef.current = editor; + }, []); + + const handleContentChange = useCallback( + (content: string) => { + onChange?.(content); + }, + [onChange], + ); + + useImperativeHandle( + ref, + () => ({ + insertText: (text: string) => { + const editor = editorRef.current; + if (!editor) return; + + editor.update(() => { + const selection = $getSelection(); + if ($isRangeSelection(selection)) { + const textNode = $createTextNode(text); + $insertNodes([textNode]); + textNode.selectEnd(); + } else { + const root = $getRoot(); + const lastChild = root.getLastChild(); + if (lastChild) { + if (lastChild.getType() === "paragraph") { + const paragraph = lastChild as ParagraphNode; + const textNode = $createTextNode(text); + paragraph.append(textNode); + textNode.selectEnd(); + } else { + const textNode = $createTextNode(text); + lastChild.insertAfter(textNode); + textNode.selectEnd(); + } + } else { + const paragraph = $createParagraphNode(); + const textNode = $createTextNode(text); + paragraph.append(textNode); + root.append(paragraph); + textNode.selectEnd(); + } + } + }); + }, + clear: () => { + const editor = editorRef.current; + if (!editor) return; + + editor.update(() => { + const root = $getRoot(); + root.clear(); + const paragraph = $createParagraphNode(); + root.append(paragraph); + paragraph.select(); + }); + }, + focus: () => { + const editor = editorRef.current; + if (!editor) return; + editor.focus(() => { + editor.update(() => { + const root = $getRoot(); + const last = root.getLastChild(); + if (!last) { + const paragraph = $createParagraphNode(); + root.append(paragraph); + paragraph.select(); + return; + } + last.selectEnd(); + }); + }); + }, + getValue: () => { + const editor = editorRef.current; + if (!editor) return ""; + let content = ""; + editor.getEditorState().read(() => { + content = $getRoot().getTextContent(); + }); + return content; + }, + }), + [], + ); + + return ( + +
*]:col-start-1 [&>*]:row-start-1", + disabled && "cursor-not-allowed opacity-50", + className, + )} + style={style} + {...props} + > + + } + placeholder={ +
+ {placeholder} +
+ } + ErrorBoundary={LexicalErrorBoundary} + /> + + + + + + + + {autoFocus && } +
+
+ ); + }, +); +ChatMessageInput.displayName = "ChatMessageInput"; + +export { ChatMessageInput }; diff --git a/site/src/pages/AgentsPage/AgentChatInput.stories.tsx b/site/src/pages/AgentsPage/AgentChatInput.stories.tsx index 09f0d4de07..4c25b79e50 100644 --- a/site/src/pages/AgentsPage/AgentChatInput.stories.tsx +++ b/site/src/pages/AgentsPage/AgentChatInput.stories.tsx @@ -16,9 +16,9 @@ const meta: Meta = { component: AgentChatInput, args: { onSend: fn(), - onChange: fn(), + onContentChange: fn(), onModelChange: fn(), - value: "", + initialValue: "", isDisabled: false, isLoading: false, selectedModel: defaultModelOptions[0].id, @@ -47,12 +47,24 @@ export const DisablesSendUntilInput: Story = { export const SendsAndClearsInput: Story = { args: { onSend: fn(), - value: "Run focused tests", + initialValue: "Run focused tests", }, play: async ({ canvasElement, args }) => { const canvas = within(canvasElement); - await userEvent.click(canvas.getByRole("button", { name: "Send" })); + // Wait for the Lexical editor to initialize and render the + // initial value text into the DOM before interacting. + const editor = canvas.getByTestId("chat-message-input"); + await waitFor(() => { + expect(editor.textContent).toBe("Run focused tests"); + }); + + const sendButton = canvas.getByRole("button", { name: "Send" }); + await waitFor(() => { + expect(sendButton).toBeEnabled(); + }); + + await userEvent.click(sendButton); await waitFor(() => { expect(args.onSend).toHaveBeenCalledWith("Run focused tests"); @@ -63,7 +75,7 @@ export const SendsAndClearsInput: Story = { export const DisabledInput: Story = { args: { isDisabled: true, - value: "Should not send", + initialValue: "Should not send", }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); @@ -75,7 +87,7 @@ export const NoModelOptions: Story = { args: { isDisabled: false, hasModelOptions: false, - value: "Model required", + initialValue: "Model required", }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); @@ -87,7 +99,7 @@ export const LoadingSpinner: Story = { args: { isDisabled: true, isLoading: true, - value: "Sending...", + initialValue: "Sending...", }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); @@ -103,7 +115,7 @@ export const LoadingDisablesSend: Story = { args: { isDisabled: false, isLoading: true, - value: "Another message", + initialValue: "Another message", }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); diff --git a/site/src/pages/AgentsPage/AgentChatInput.tsx b/site/src/pages/AgentsPage/AgentChatInput.tsx index d4e25dde22..6f867dbff5 100644 --- a/site/src/pages/AgentsPage/AgentChatInput.tsx +++ b/site/src/pages/AgentsPage/AgentChatInput.tsx @@ -4,6 +4,10 @@ import { type ModelSelectorOption, } from "components/ai-elements"; import { Button } from "components/Button/Button"; +import { + ChatMessageInput, + type ChatMessageInputRef, +} from "components/ChatMessageInput/ChatMessageInput"; import { Tooltip, TooltipContent, @@ -16,12 +20,13 @@ import { Square, XIcon, } from "lucide-react"; -import { memo, type ReactNode, useRef } from "react"; -import TextareaAutosize from "react-textarea-autosize"; +import { memo, type ReactNode, useCallback, useRef, useState } from "react"; import { cn } from "utils/cn"; import { formatProviderLabel } from "./modelOptions"; import { QueuedMessagesList } from "./QueuedMessagesList"; +export type { ChatMessageInputRef } from "components/ChatMessageInput/ChatMessageInput"; + export interface AgentContextUsage { readonly usedTokens?: number; readonly contextLimitTokens?: number; @@ -39,9 +44,12 @@ interface AgentChatInputProps { placeholder?: string; isDisabled: boolean; isLoading: boolean; - // Controlled input value. The parent owns the text state. - value: string; - onChange: (value: string) => void; + // Ref for the Lexical editor, exposed for imperative access. + inputRef?: React.Ref; + // Initial text to seed the editor with. + initialValue?: string; + // Called on every text change inside the editor. + onContentChange?: (content: string) => void; // Model selector. selectedModel: string; onModelChange: (value: string) => void; @@ -208,8 +216,9 @@ export const AgentChatInput = memo( placeholder = "Type a message...", isDisabled, isLoading, - value, - onChange, + inputRef, + initialValue, + onContentChange, selectedModel, onModelChange, modelOptions, @@ -232,18 +241,71 @@ export const AgentChatInput = memo( contextUsage, sticky = false, }) => { - const textareaRef = useRef(null); + const internalRef = useRef(null); - const canSend = - !isDisabled && !isLoading && hasModelOptions && !!value.trim(); + // Merge the external inputRef with our internal ref so both + // point to the same ChatMessageInputRef instance. + const setRef = useCallback( + (instance: ChatMessageInputRef | null) => { + ( + internalRef as React.MutableRefObject + ).current = instance; + if (typeof inputRef === "function") { + inputRef(instance); + } else if (inputRef && typeof inputRef === "object") { + ( + inputRef as React.MutableRefObject + ).current = instance; + } + }, + [inputRef], + ); - const handleSubmit = () => { - if (!canSend) { + // Track whether the editor has content so we can gate the + // send button without a controlled value prop. + const [hasContent, setHasContent] = useState(() => !!initialValue?.trim()); + + const handleContentChange = useCallback( + (content: string) => { + setHasContent(!!content.trim()); + onContentChange?.(content); + }, + [onContentChange], + ); + + const canSend = !isDisabled && !isLoading && hasModelOptions && hasContent; + + const handleSubmit = useCallback(() => { + const text = internalRef.current?.getValue()?.trim() ?? ""; + + // If the input is empty and there are queued messages, + // promote the first one instead of submitting. + if ( + !text && + !isDisabled && + !isLoading && + queuedMessages.length > 0 && + onPromoteQueuedMessage + ) { + void onPromoteQueuedMessage(queuedMessages[0].id); return; } - onSend(value.trim()); - textareaRef.current?.focus(); - }; + + if (!text || isDisabled || isLoading || !hasModelOptions) { + return; + } + + onSend(text); + internalRef.current?.clear(); + internalRef.current?.focus(); + }, [ + isDisabled, + isLoading, + hasModelOptions, + onSend, + queuedMessages, + onPromoteQueuedMessage, + ]); const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Escape") { @@ -257,23 +319,6 @@ export const AgentChatInput = memo( e.preventDefault(); onInterrupt(); } - return; - } - if (e.key === "Enter" && !e.shiftKey) { - e.preventDefault(); - // If the input is empty and there are queued messages, - // promote the first one instead of submitting. - if ( - !value.trim() && - !isDisabled && - !isLoading && - queuedMessages.length > 0 && - onPromoteQueuedMessage - ) { - void onPromoteQueuedMessage(queuedMessages[0].id); - return; - } - handleSubmit(); } }; @@ -302,7 +347,10 @@ export const AgentChatInput = memo( className="mb-2" /> )} -
+
{editingQueuedMessageID !== null && (
@@ -340,16 +388,17 @@ export const AgentChatInput = memo(
)} - onChange(e.target.value)} - onKeyDown={handleKeyDown} + initialValue={initialValue} + onChange={handleContentChange} + onEnter={handleSubmit} disabled={isDisabled} - minRows={4} + rows={4} + autoFocus />
diff --git a/site/src/pages/AgentsPage/AgentDetail.tsx b/site/src/pages/AgentsPage/AgentDetail.tsx index 52d09596f1..5d302d093d 100644 --- a/site/src/pages/AgentsPage/AgentDetail.tsx +++ b/site/src/pages/AgentsPage/AgentDetail.tsx @@ -29,7 +29,7 @@ import { useNavigate, useOutletContext, useParams } from "react-router"; import { toast } from "sonner"; import { cn } from "utils/cn"; import { pageTitle } from "utils/page"; -import { AgentChatInput } from "./AgentChatInput"; +import { AgentChatInput, type ChatMessageInputRef } from "./AgentChatInput"; import { selectChatStatus, selectHasStreamState, @@ -204,8 +204,9 @@ interface AgentDetailInputProps { modelCatalogStatusMessage: string | null; // Controlled input value and editing state, owned by the // conversation component. - inputValue: string; - onInputChange: (value: string) => void; + inputRef?: React.Ref; + initialValue?: string; + onContentChange?: (content: string) => void; editingQueuedMessageID: number | null; onStartQueueEdit: (id: number, text: string) => void; onCancelQueueEdit: () => void; @@ -230,8 +231,9 @@ const AgentDetailInput: FC = ({ modelSelectorPlaceholder, inputStatusText, modelCatalogStatusMessage, - inputValue, - onInputChange, + inputRef, + initialValue, + onContentChange, editingQueuedMessageID, onStartQueueEdit, onCancelQueueEdit, @@ -264,8 +266,9 @@ const AgentDetailInput: FC = ({ return ( = ({ modelCatalogStatusMessage, savingMessageId, }) => { - const [inputValue, setInputValue] = useState(""); + const inputValueRef = useRef(""); + const chatInputRef = useRef(null); + const [editorInitialValue, setEditorInitialValue] = useState(""); // -- History editing state -- const [editingMessageId, setEditingMessageId] = useState(null); @@ -346,16 +351,18 @@ const AgentDetailConversation: FC = ({ const handleEditUserMessage = useCallback( (messageId: number, text: string) => { setDraftBeforeHistoryEdit((prev) => - editingMessageId !== null ? prev : inputValue, + editingMessageId !== null ? prev : inputValueRef.current, ); setEditingMessageId(messageId); - setInputValue(text); + setEditorInitialValue(text); + inputValueRef.current = text; }, - [editingMessageId, inputValue], + [editingMessageId], ); const handleCancelHistoryEdit = useCallback(() => { - setInputValue(draftBeforeHistoryEdit ?? ""); + setEditorInitialValue(draftBeforeHistoryEdit ?? ""); + inputValueRef.current = draftBeforeHistoryEdit ?? ""; setEditingMessageId(null); setDraftBeforeHistoryEdit(null); }, [draftBeforeHistoryEdit]); @@ -371,16 +378,18 @@ const AgentDetailConversation: FC = ({ const handleStartQueueEdit = useCallback( (id: number, text: string) => { setDraftBeforeQueueEdit((prev) => - editingQueuedMessageID === null ? inputValue : prev, + editingQueuedMessageID === null ? inputValueRef.current : prev, ); setEditingQueuedMessageID(id); - setInputValue(text); + setEditorInitialValue(text); + inputValueRef.current = text; }, - [editingQueuedMessageID, inputValue], + [editingQueuedMessageID], ); const handleCancelQueueEdit = useCallback(() => { - setInputValue(draftBeforeQueueEdit ?? ""); + setEditorInitialValue(draftBeforeQueueEdit ?? ""); + inputValueRef.current = draftBeforeQueueEdit ?? ""; setEditingQueuedMessageID(null); setDraftBeforeQueueEdit(null); }, [draftBeforeQueueEdit]); @@ -394,7 +403,8 @@ const AgentDetailConversation: FC = ({ const queueEditID = editingQueuedMessageID; // Clear input and editing state optimistically. - setInputValue(""); + setEditorInitialValue(""); + inputValueRef.current = ""; if (editingMessageId !== null) { setEditingMessageId(null); setDraftBeforeHistoryEdit(null); @@ -412,7 +422,8 @@ const AgentDetailConversation: FC = ({ }) .catch(() => { // Restore input so the user can retry. - setInputValue(message); + setEditorInitialValue(message); + inputValueRef.current = message; }); }, [editingMessageId, editingQueuedMessageID, onDeleteQueuedMessage, onSend], @@ -445,8 +456,11 @@ const AgentDetailConversation: FC = ({ modelSelectorPlaceholder={modelSelectorPlaceholder} inputStatusText={inputStatusText} modelCatalogStatusMessage={modelCatalogStatusMessage} - inputValue={inputValue} - onInputChange={setInputValue} + inputRef={chatInputRef} + initialValue={editorInitialValue} + onContentChange={(content) => { + inputValueRef.current = content; + }} editingQueuedMessageID={editingQueuedMessageID} onStartQueueEdit={handleStartQueueEdit} onCancelQueueEdit={handleCancelQueueEdit} @@ -896,8 +910,7 @@ const AgentDetail: FC = () => {
{}} - value="" - onChange={() => {}} + initialValue="" isDisabled={isInputDisabled} isLoading={false} selectedModel={selectedModel} diff --git a/site/src/pages/AgentsPage/AgentsPage.tsx b/site/src/pages/AgentsPage/AgentsPage.tsx index efe3fcd764..ae44e3be04 100644 --- a/site/src/pages/AgentsPage/AgentsPage.tsx +++ b/site/src/pages/AgentsPage/AgentsPage.tsx @@ -511,12 +511,13 @@ export const AgentsEmptyState: FC = ({ isConfigureAgentsDialogOpen, onConfigureAgentsDialogOpenChange, }) => { - const [inputValue, setInputValue] = useState(() => { + const [initialInputValue] = useState(() => { if (typeof window === "undefined") { return ""; } return localStorage.getItem(emptyInputStorageKey) ?? ""; }); + const inputValueRef = useRef(initialInputValue); const initialSystemPrompt = () => { if (typeof window === "undefined") { return ""; @@ -659,10 +660,10 @@ export const AgentsEmptyState: FC = ({ } }; - const handleInputChange = useCallback((value: string) => { - setInputValue(value); + const handleContentChange = useCallback((content: string) => { + inputValueRef.current = content; if (typeof window !== "undefined") { - localStorage.setItem(emptyInputStorageKey, value); + localStorage.setItem(emptyInputStorageKey, content); } }, []); const handleModelChange = useCallback((value: string) => { @@ -720,8 +721,8 @@ export const AgentsEmptyState: FC = ({ placeholder="Ask Coder to build, fix bugs, or explore your project..." isDisabled={isCreating} isLoading={isCreating} - value={inputValue} - onChange={handleInputChange} + initialValue={initialInputValue} + onContentChange={handleContentChange} selectedModel={selectedModel} onModelChange={handleModelChange} modelOptions={modelOptions}