diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 30d6b006..2186855f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -155,6 +155,30 @@ importers: '@tauri-apps/plugin-opener': specifier: ^2.5.2 version: 2.5.3 + '@tiptap/core': + specifier: ^3.20.4 + version: 3.20.4(@tiptap/pm@3.20.4) + '@tiptap/extension-link': + specifier: ^3.20.4 + version: 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4) + '@tiptap/extension-placeholder': + specifier: ^3.20.4 + version: 3.20.4(@tiptap/extensions@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)) + '@tiptap/extension-task-item': + specifier: ^3.20.4 + version: 3.20.4(@tiptap/extension-list@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)) + '@tiptap/extension-task-list': + specifier: ^3.20.4 + version: 3.20.4(@tiptap/extension-list@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)) + '@tiptap/pm': + specifier: ^3.20.4 + version: 3.20.4 + '@tiptap/react': + specifier: ^3.20.4 + version: 3.20.4(@floating-ui/dom@1.7.6)(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@tiptap/starter-kit': + specifier: ^3.20.4 + version: 3.20.4 '@xterm/addon-fit': specifier: ^0.12.0-beta.103 version: 0.12.0-beta.141(@xterm/xterm@6.1.0-beta.141) @@ -658,6 +682,15 @@ packages: '@fastify/websocket@11.2.0': resolution: {integrity: sha512-3HrDPbAG1CzUCqnslgJxppvzaAZffieOVbLp1DAy1huCSynUWPifSvfdEDUR8HlJLp3sp1A36uOM2tJogADS8w==} + '@floating-ui/core@1.7.5': + resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} + + '@floating-ui/dom@1.7.6': + resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} + + '@floating-ui/utils@0.2.11': + resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + '@iconify-json/mdi@1.2.3': resolution: {integrity: sha512-O3cLwbDOK7NNDf2ihaQOH5F9JglnulNDFV7WprU2dSoZu3h3cWH//h74uQAB87brHmvFVxIOkuBX2sZSzYhScg==} @@ -1143,6 +1176,9 @@ packages: engines: {node: '>=18'} hasBin: true + '@remirror/core-constants@3.0.0': + resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==} + '@rolldown/pluginutils@1.0.0-beta.27': resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} @@ -1370,6 +1406,170 @@ packages: '@tauri-apps/plugin-opener@2.5.3': resolution: {integrity: sha512-CCcUltXMOfUEArbf3db3kCE7Ggy1ExBEBl51Ko2ODJ6GDYHRp1nSNlQm5uNCFY5k7/ufaK5Ib3Du/Zir19IYQQ==} + '@tiptap/core@3.20.4': + resolution: {integrity: sha512-3i/DG89TFY/b34T5P+j35UcjYuB5d3+9K8u6qID+iUqNPiza015HPIZLuPfE5elNwVdV3EXIoPo0LLeBLgXXAg==} + peerDependencies: + '@tiptap/pm': ^3.20.4 + + '@tiptap/extension-blockquote@3.20.4': + resolution: {integrity: sha512-9sskyyhYj2oKat//lyZVXCp9YrPt4oJAZnGHYWXS0xlskjsLElrfKKlM4vpbhGss3VrhQRoEGqWLnIaJYPF1zw==} + peerDependencies: + '@tiptap/core': ^3.20.4 + + '@tiptap/extension-bold@3.20.4': + resolution: {integrity: sha512-Md7/mNAeJCY+VLJc8JRGI+8XkVPKiOGB1NgqQPdh3aYtxXQDChQOZoJEQl6TuudDxZ85bLZB67NjZlx3jo8/0g==} + peerDependencies: + '@tiptap/core': ^3.20.4 + + '@tiptap/extension-bubble-menu@3.20.4': + resolution: {integrity: sha512-EXywPlI8wjPcAb8ozymgVhjtMjFrnhtoyNTy8ZcObdpUi5CdO9j892Y7aPbKe5hLhlDpvJk7rMfir4FFKEmfng==} + peerDependencies: + '@tiptap/core': ^3.20.4 + '@tiptap/pm': ^3.20.4 + + '@tiptap/extension-bullet-list@3.20.4': + resolution: {integrity: sha512-1RTGrur1EKoxfnLZ3M6xeNj8GITAz74jH2DHGcjLsd2Xr7Q7BozGaIq6GkkvKguMwbI1zCOxTHFCpUETXAIQQA==} + peerDependencies: + '@tiptap/extension-list': ^3.20.4 + + '@tiptap/extension-code-block@3.20.4': + resolution: {integrity: sha512-Zlw3FrXTy01+o1yISeX/LC+iJeHA+ym602bMXGmtA6lyl7QSOSO7WExweJ6xeJGhbCjldwT5al6fkRAs8iGJZg==} + peerDependencies: + '@tiptap/core': ^3.20.4 + '@tiptap/pm': ^3.20.4 + + '@tiptap/extension-code@3.20.4': + resolution: {integrity: sha512-7j8Hi964bH1SZ9oLdZC1fkqWz27mliSDV7M8lmL/M14+Qw42D/VOAKS4Aw9OCFtHMlTsjLR6qsoVxL8Lpkt6NA==} + peerDependencies: + '@tiptap/core': ^3.20.4 + + '@tiptap/extension-document@3.20.4': + resolution: {integrity: sha512-zF1CIFVLt8MfSpWWnPwtGyxPOsT0xYM2qJKcXf2yZcTG37wDKmUi6heG53vGigIavbQlLaAFvs+1mNdOu2x/0A==} + peerDependencies: + '@tiptap/core': ^3.20.4 + + '@tiptap/extension-dropcursor@3.20.4': + resolution: {integrity: sha512-TgMwvZ8myXYdmd6bUV7qkpZXv7ZUiSmX/8eo+iPEzYo2CnDLAGvDKgC50nfq/g87SDvfBgPuAiBfFvsMQQWaTw==} + peerDependencies: + '@tiptap/extensions': ^3.20.4 + + '@tiptap/extension-floating-menu@3.20.4': + resolution: {integrity: sha512-AaPTFhoO8DBIElJyd/RTVJjkctvJuL+GHURX0npbtTxXq5HXbebVwf2ARNR7jMd/GThsmBaNJiGxZg4A2oeDqQ==} + peerDependencies: + '@floating-ui/dom': ^1.0.0 + '@tiptap/core': ^3.20.4 + '@tiptap/pm': ^3.20.4 + + '@tiptap/extension-gapcursor@3.20.4': + resolution: {integrity: sha512-JJ6f1iQ1e0s4kISgq55U3UYGwWV/N9f0PYMtB6e3L+SBQjXnywaLK0g6vfN6IvTCC2vdIuqeSOX8VlSO97sJLw==} + peerDependencies: + '@tiptap/extensions': ^3.20.4 + + '@tiptap/extension-hard-break@3.20.4': + resolution: {integrity: sha512-gJbq58d8zB1gzyqVEopowej5CpW4/Fpg6oGJvlZxaCukqd0gJRWGC89K+jE62YA1Td4sfcKrekKvN7jm2y/ZUg==} + peerDependencies: + '@tiptap/core': ^3.20.4 + + '@tiptap/extension-heading@3.20.4': + resolution: {integrity: sha512-xsnkmTGggJc5P2iCwS1lv8KFG31xC/GNPJKoi/3UH67j/lKDhA3AdtshsLeyv2FKtTtYDb8oV0IqzHB1MM6a7w==} + peerDependencies: + '@tiptap/core': ^3.20.4 + + '@tiptap/extension-horizontal-rule@3.20.4': + resolution: {integrity: sha512-y6joCi49haAA0bo3EGUY+dWUMHH1GPUc84hxrBY/0pMs+Bn+kQ1+DQJErZDTWGJrlHPWU/yekBZT72SNdp0DNA==} + peerDependencies: + '@tiptap/core': ^3.20.4 + '@tiptap/pm': ^3.20.4 + + '@tiptap/extension-italic@3.20.4': + resolution: {integrity: sha512-4ZqiWr7cmqPFux8tj1ZLiYytyWf343IvQemNX6AvVWvscrJcrfj3YX4Le2BA0RW3A3M6RpLQXXozuF8vxYFDeQ==} + peerDependencies: + '@tiptap/core': ^3.20.4 + + '@tiptap/extension-link@3.20.4': + resolution: {integrity: sha512-JNDSkWrVdb8NSvbQXwHWvK5tCMbTWwOHFOweknQZ1JPK4dei9FJVofYQaHyW4bJBdcCjds3NZSnXE8DM9iAWmg==} + peerDependencies: + '@tiptap/core': ^3.20.4 + '@tiptap/pm': ^3.20.4 + + '@tiptap/extension-list-item@3.20.4': + resolution: {integrity: sha512-QoTc5RACXaZF+vIIBBxjGO7D0oWFUDgBKJCpvUZ0CoGGKosnfe4a9I5THFyLj4201cf0oUqgf1oZhTqETGxlVw==} + peerDependencies: + '@tiptap/extension-list': ^3.20.4 + + '@tiptap/extension-list-keymap@3.20.4': + resolution: {integrity: sha512-RIqXM649+8IP7p/KVfaGlJiwjCylm1m6OPlaoM3K8O7oEOGRQzNeexexECCD2jsXRxew4E+vBNMD2orXqJmu8A==} + peerDependencies: + '@tiptap/extension-list': ^3.20.4 + + '@tiptap/extension-list@3.20.4': + resolution: {integrity: sha512-X+5plTKhOioNcQ4KsAFJJSb/3+zR8Xhdpow4HzXtoV1KcbdDey1fhZdpsfkbrzCL0s6/wAgwZuAchCK7HujurQ==} + peerDependencies: + '@tiptap/core': ^3.20.4 + '@tiptap/pm': ^3.20.4 + + '@tiptap/extension-ordered-list@3.20.4': + resolution: {integrity: sha512-3budNL8BgBon3TcXZ4hjT0YpFvx1Ka3uSIECKDxHgES+OQcR+6cagxSb60gFEccf3Dr0PIwcVTY6g14lC1qKRQ==} + peerDependencies: + '@tiptap/extension-list': ^3.20.4 + + '@tiptap/extension-paragraph@3.20.4': + resolution: {integrity: sha512-lm6fOScWuZAF/Sfp97igUwFd3L1QHIVLAWP5NVdh0DTLrEIt4rMBmsww+yOpMQRhvz2uTgMbMXynrimhzi/QVw==} + peerDependencies: + '@tiptap/core': ^3.20.4 + + '@tiptap/extension-placeholder@3.20.4': + resolution: {integrity: sha512-GB0KWtqm83YHG8cnqBLijvUBm+xvLfQHDfFRRH2fb3EzH3eIsM9jKRC31ADT27RSV1zVpHMFGcP3/pWpdrN1Lw==} + peerDependencies: + '@tiptap/extensions': ^3.20.4 + + '@tiptap/extension-strike@3.20.4': + resolution: {integrity: sha512-It1Px9uDGTsVqyyg6cy7DigLoenljpQwqdI0jssM7QclZrHnsrye9fZxBBiiuCzzV1305MxKgHvratkHwqmVNA==} + peerDependencies: + '@tiptap/core': ^3.20.4 + + '@tiptap/extension-task-item@3.20.4': + resolution: {integrity: sha512-mEWyAtZ61USZnKyLDxi2DtnSREfW0yUFXDOFWstNg1i6hva197BuAy6VRQMQxTOq+cFAgAt1MEZKanW0Obsa+g==} + peerDependencies: + '@tiptap/extension-list': ^3.20.4 + + '@tiptap/extension-task-list@3.20.4': + resolution: {integrity: sha512-QvLrpffkxkr7TTgMmk6fnPAE34HYrUosHiuZJpRK008MuJDOoANblS221M4lLuRE73w3KI7hd/fi2CliBcCC4A==} + peerDependencies: + '@tiptap/extension-list': ^3.20.4 + + '@tiptap/extension-text@3.20.4': + resolution: {integrity: sha512-jchJcBZixDEO2J66Zx5dchsI2mA6IYsROqF8P1poxL4ienH7RVQRCTsBNnSfIeOtREKKWeOU/tEs5fcpvvGwIQ==} + peerDependencies: + '@tiptap/core': ^3.20.4 + + '@tiptap/extension-underline@3.20.4': + resolution: {integrity: sha512-0OjMc3FDujX16G+jhvqcY/mLot8SrNtDu8ggUwNLAfiI/QIvMVgk7giFD71DATC/4Nb8i/iwAEegTD8MxBIXCg==} + peerDependencies: + '@tiptap/core': ^3.20.4 + + '@tiptap/extensions@3.20.4': + resolution: {integrity: sha512-8p6hVT65DjuQjtEdlH6ewX9SOJHlVQAOee3sWIJQmeJNRnZNvqPIBLleebUqDiljNTpxBv6s6QWkSTKgf3btwg==} + peerDependencies: + '@tiptap/core': ^3.20.4 + '@tiptap/pm': ^3.20.4 + + '@tiptap/pm@3.20.4': + resolution: {integrity: sha512-rCHYSBToilBEuI6PtjziHDdRkABH/XqwJ7dG4Amn/SD3yGiZKYCiEApQlTUS2zZeo8DsLeuqqqB4vEOeD4OEPg==} + + '@tiptap/react@3.20.4': + resolution: {integrity: sha512-1B8iWsHWwb5TeyVaUs8BRPzwWo4PsLQcl03urHaz0zTJ8DauopqvxzV3+lem1OkzRHn7wnrapDvwmIGoROCaQw==} + peerDependencies: + '@tiptap/core': ^3.20.4 + '@tiptap/pm': ^3.20.4 + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + '@types/react-dom': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@tiptap/starter-kit@3.20.4': + resolution: {integrity: sha512-WcyK6hsTl8eBsQhQ+d9Sq8fYZKOYdL+D45MyH3hz583elXqJlW3h3JPFYb0o87gddGxn8Mm57OA/gA1zEdeDMw==} + '@tootallnate/quickjs-emscripten@0.23.0': resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==} @@ -1526,9 +1726,18 @@ packages: '@types/katex@0.16.8': resolution: {integrity: sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg==} + '@types/linkify-it@5.0.0': + resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} + + '@types/markdown-it@14.1.2': + resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} + '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + '@types/mdurl@2.0.0': + resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} + '@types/mocha@10.0.10': resolution: {integrity: sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==} @@ -1579,6 +1788,9 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/use-sync-external-store@0.0.6': + resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} + '@types/which@2.0.2': resolution: {integrity: sha512-113D3mDkZDjo+EeUEHCFy0qniNc1ZpecGiAU7WSo7YDoSzolZIQKpYFHrPpjkB2nuyahcKfrmLXeQlh7gqJYdw==} @@ -2649,6 +2861,10 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-equals@5.4.0: + resolution: {integrity: sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==} + engines: {node: '>=6.0.0'} + fast-fifo@1.3.2: resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} @@ -3192,6 +3408,12 @@ packages: resolution: {integrity: sha512-wM1+Z03eypVAVUCE7QdSqpVIvelbOakn1M0bPDoA4SGWPx3sNDVUiMo3L6To6WWGClB7VyXnhQ4Sn7gxiJbE6A==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + linkify-it@5.0.0: + resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} + + linkifyjs@4.3.2: + resolution: {integrity: sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==} + listenercount@1.0.1: resolution: {integrity: sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==} @@ -3288,6 +3510,10 @@ packages: make-error@1.3.6: resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + markdown-it@14.1.1: + resolution: {integrity: sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==} + hasBin: true + markdown-table@3.0.4: resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} @@ -3347,6 +3573,9 @@ packages: mdast-util-to-string@4.0.0: resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + mdurl@2.0.0: + resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + mermaid@11.12.2: resolution: {integrity: sha512-n34QPDPEKmaeCG4WDMGy0OT6PSyxKCfy2pJgShP+Qow2KLrvWjclwbc3yXfSIf4BanqWEhQEpngWwNp/XhZt6w==} @@ -3577,6 +3806,9 @@ packages: once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + orderedmap@2.1.1: + resolution: {integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==} + p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} @@ -3772,6 +4004,64 @@ packages: property-information@7.1.0: resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + prosemirror-changeset@2.4.0: + resolution: {integrity: sha512-LvqH2v7Q2SF6yxatuPP2e8vSUKS/L+xAU7dPDC4RMyHMhZoGDfBC74mYuyYF4gLqOEG758wajtyhNnsTkuhvng==} + + prosemirror-collab@1.3.1: + resolution: {integrity: sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==} + + prosemirror-commands@1.7.1: + resolution: {integrity: sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==} + + prosemirror-dropcursor@1.8.2: + resolution: {integrity: sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==} + + prosemirror-gapcursor@1.4.1: + resolution: {integrity: sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==} + + prosemirror-history@1.5.0: + resolution: {integrity: sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==} + + prosemirror-inputrules@1.5.1: + resolution: {integrity: sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==} + + prosemirror-keymap@1.2.3: + resolution: {integrity: sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==} + + prosemirror-markdown@1.13.4: + resolution: {integrity: sha512-D98dm4cQ3Hs6EmjK500TdAOew4Z03EV71ajEFiWra3Upr7diytJsjF4mPV2dW+eK5uNectiRj0xFxYI9NLXDbw==} + + prosemirror-menu@1.3.0: + resolution: {integrity: sha512-TImyPXCHPcDsSka2/lwJ6WjTASr4re/qWq1yoTTuLOqfXucwF6VcRa2LWCkM/EyTD1UO3CUwiH8qURJoWJRxwg==} + + prosemirror-model@1.25.4: + resolution: {integrity: sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==} + + prosemirror-schema-basic@1.2.4: + resolution: {integrity: sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ==} + + prosemirror-schema-list@1.5.1: + resolution: {integrity: sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==} + + prosemirror-state@1.4.4: + resolution: {integrity: sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==} + + prosemirror-tables@1.8.5: + resolution: {integrity: sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==} + + prosemirror-trailing-node@3.0.0: + resolution: {integrity: sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==} + peerDependencies: + prosemirror-model: ^1.22.1 + prosemirror-state: ^1.4.2 + prosemirror-view: ^1.33.8 + + prosemirror-transform@1.11.0: + resolution: {integrity: sha512-4I7Ce4KpygXb9bkiPS3hTEk4dSHorfRw8uI0pE8IhxlK2GXsqv5tIA7JUSxtSu7u8APVOTtbUBxTmnHIxVkIJw==} + + prosemirror-view@1.41.7: + resolution: {integrity: sha512-jUwKNCEIGiqdvhlS91/2QAg21e4dfU5bH2iwmSDQeosXJgKF7smG0YSplOWK0cjSNgIqXe7VXqo7EIfUFJdt3w==} + proxy-agent@6.3.1: resolution: {integrity: sha512-Rb5RVBy1iyqOtNl15Cw/llpeLH8bsb37gM1FUfKQ+Wck6xHlbAhWGUFiTRHtkjqGTA5pSHz6+0hrPW/oECihPQ==} engines: {node: '>= 14'} @@ -3786,6 +4076,10 @@ packages: pump@3.0.4: resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + punycode.js@2.3.1: + resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} + engines: {node: '>=6'} + puppeteer-core@21.11.0: resolution: {integrity: sha512-ArbnyA3U5SGHokEvkfWjW+O8hOxV1RSJxOgriX/3A4xZRqixt9ZFHD0yPgZQF05Qj0oAqi8H/7stDorjoHY90Q==} engines: {node: '>=16.13.2'} @@ -3969,6 +4263,9 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + rope-sequence@1.3.4: + resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==} + roughjs@4.6.6: resolution: {integrity: sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==} @@ -4337,6 +4634,9 @@ packages: resolution: {integrity: sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug==} hasBin: true + uc.micro@2.1.0: + resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} + ufo@1.6.3: resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} @@ -5086,6 +5386,20 @@ snapshots: - bufferutil - utf-8-validate + '@floating-ui/core@1.7.5': + dependencies: + '@floating-ui/utils': 0.2.11 + optional: true + + '@floating-ui/dom@1.7.6': + dependencies: + '@floating-ui/core': 1.7.5 + '@floating-ui/utils': 0.2.11 + optional: true + + '@floating-ui/utils@0.2.11': + optional: true + '@iconify-json/mdi@1.2.3': dependencies: '@iconify/types': 2.0.0 @@ -5537,6 +5851,8 @@ snapshots: - react-native-b4a - supports-color + '@remirror/core-constants@3.0.0': {} + '@rolldown/pluginutils@1.0.0-beta.27': {} '@rollup/rollup-android-arm-eabi@4.57.1': @@ -5687,6 +6003,195 @@ snapshots: dependencies: '@tauri-apps/api': 2.10.1 + '@tiptap/core@3.20.4(@tiptap/pm@3.20.4)': + dependencies: + '@tiptap/pm': 3.20.4 + + '@tiptap/extension-blockquote@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))': + dependencies: + '@tiptap/core': 3.20.4(@tiptap/pm@3.20.4) + + '@tiptap/extension-bold@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))': + dependencies: + '@tiptap/core': 3.20.4(@tiptap/pm@3.20.4) + + '@tiptap/extension-bubble-menu@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)': + dependencies: + '@floating-ui/dom': 1.7.6 + '@tiptap/core': 3.20.4(@tiptap/pm@3.20.4) + '@tiptap/pm': 3.20.4 + optional: true + + '@tiptap/extension-bullet-list@3.20.4(@tiptap/extension-list@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4))': + dependencies: + '@tiptap/extension-list': 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4) + + '@tiptap/extension-code-block@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)': + dependencies: + '@tiptap/core': 3.20.4(@tiptap/pm@3.20.4) + '@tiptap/pm': 3.20.4 + + '@tiptap/extension-code@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))': + dependencies: + '@tiptap/core': 3.20.4(@tiptap/pm@3.20.4) + + '@tiptap/extension-document@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))': + dependencies: + '@tiptap/core': 3.20.4(@tiptap/pm@3.20.4) + + '@tiptap/extension-dropcursor@3.20.4(@tiptap/extensions@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4))': + dependencies: + '@tiptap/extensions': 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4) + + '@tiptap/extension-floating-menu@3.20.4(@floating-ui/dom@1.7.6)(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)': + dependencies: + '@floating-ui/dom': 1.7.6 + '@tiptap/core': 3.20.4(@tiptap/pm@3.20.4) + '@tiptap/pm': 3.20.4 + optional: true + + '@tiptap/extension-gapcursor@3.20.4(@tiptap/extensions@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4))': + dependencies: + '@tiptap/extensions': 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4) + + '@tiptap/extension-hard-break@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))': + dependencies: + '@tiptap/core': 3.20.4(@tiptap/pm@3.20.4) + + '@tiptap/extension-heading@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))': + dependencies: + '@tiptap/core': 3.20.4(@tiptap/pm@3.20.4) + + '@tiptap/extension-horizontal-rule@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)': + dependencies: + '@tiptap/core': 3.20.4(@tiptap/pm@3.20.4) + '@tiptap/pm': 3.20.4 + + '@tiptap/extension-italic@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))': + dependencies: + '@tiptap/core': 3.20.4(@tiptap/pm@3.20.4) + + '@tiptap/extension-link@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)': + dependencies: + '@tiptap/core': 3.20.4(@tiptap/pm@3.20.4) + '@tiptap/pm': 3.20.4 + linkifyjs: 4.3.2 + + '@tiptap/extension-list-item@3.20.4(@tiptap/extension-list@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4))': + dependencies: + '@tiptap/extension-list': 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4) + + '@tiptap/extension-list-keymap@3.20.4(@tiptap/extension-list@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4))': + dependencies: + '@tiptap/extension-list': 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4) + + '@tiptap/extension-list@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)': + dependencies: + '@tiptap/core': 3.20.4(@tiptap/pm@3.20.4) + '@tiptap/pm': 3.20.4 + + '@tiptap/extension-ordered-list@3.20.4(@tiptap/extension-list@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4))': + dependencies: + '@tiptap/extension-list': 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4) + + '@tiptap/extension-paragraph@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))': + dependencies: + '@tiptap/core': 3.20.4(@tiptap/pm@3.20.4) + + '@tiptap/extension-placeholder@3.20.4(@tiptap/extensions@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4))': + dependencies: + '@tiptap/extensions': 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4) + + '@tiptap/extension-strike@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))': + dependencies: + '@tiptap/core': 3.20.4(@tiptap/pm@3.20.4) + + '@tiptap/extension-task-item@3.20.4(@tiptap/extension-list@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4))': + dependencies: + '@tiptap/extension-list': 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4) + + '@tiptap/extension-task-list@3.20.4(@tiptap/extension-list@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4))': + dependencies: + '@tiptap/extension-list': 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4) + + '@tiptap/extension-text@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))': + dependencies: + '@tiptap/core': 3.20.4(@tiptap/pm@3.20.4) + + '@tiptap/extension-underline@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))': + dependencies: + '@tiptap/core': 3.20.4(@tiptap/pm@3.20.4) + + '@tiptap/extensions@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)': + dependencies: + '@tiptap/core': 3.20.4(@tiptap/pm@3.20.4) + '@tiptap/pm': 3.20.4 + + '@tiptap/pm@3.20.4': + dependencies: + prosemirror-changeset: 2.4.0 + prosemirror-collab: 1.3.1 + prosemirror-commands: 1.7.1 + prosemirror-dropcursor: 1.8.2 + prosemirror-gapcursor: 1.4.1 + prosemirror-history: 1.5.0 + prosemirror-inputrules: 1.5.1 + prosemirror-keymap: 1.2.3 + prosemirror-markdown: 1.13.4 + prosemirror-menu: 1.3.0 + prosemirror-model: 1.25.4 + prosemirror-schema-basic: 1.2.4 + prosemirror-schema-list: 1.5.1 + prosemirror-state: 1.4.4 + prosemirror-tables: 1.8.5 + prosemirror-trailing-node: 3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.7) + prosemirror-transform: 1.11.0 + prosemirror-view: 1.41.7 + + '@tiptap/react@3.20.4(@floating-ui/dom@1.7.6)(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@tiptap/core': 3.20.4(@tiptap/pm@3.20.4) + '@tiptap/pm': 3.20.4 + '@types/react': 18.3.27 + '@types/react-dom': 18.3.7(@types/react@18.3.27) + '@types/use-sync-external-store': 0.0.6 + fast-equals: 5.4.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + use-sync-external-store: 1.6.0(react@18.3.1) + optionalDependencies: + '@tiptap/extension-bubble-menu': 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4) + '@tiptap/extension-floating-menu': 3.20.4(@floating-ui/dom@1.7.6)(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4) + transitivePeerDependencies: + - '@floating-ui/dom' + + '@tiptap/starter-kit@3.20.4': + dependencies: + '@tiptap/core': 3.20.4(@tiptap/pm@3.20.4) + '@tiptap/extension-blockquote': 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4)) + '@tiptap/extension-bold': 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4)) + '@tiptap/extension-bullet-list': 3.20.4(@tiptap/extension-list@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)) + '@tiptap/extension-code': 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4)) + '@tiptap/extension-code-block': 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4) + '@tiptap/extension-document': 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4)) + '@tiptap/extension-dropcursor': 3.20.4(@tiptap/extensions@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)) + '@tiptap/extension-gapcursor': 3.20.4(@tiptap/extensions@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)) + '@tiptap/extension-hard-break': 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4)) + '@tiptap/extension-heading': 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4)) + '@tiptap/extension-horizontal-rule': 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4) + '@tiptap/extension-italic': 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4)) + '@tiptap/extension-link': 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4) + '@tiptap/extension-list': 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4) + '@tiptap/extension-list-item': 3.20.4(@tiptap/extension-list@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)) + '@tiptap/extension-list-keymap': 3.20.4(@tiptap/extension-list@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)) + '@tiptap/extension-ordered-list': 3.20.4(@tiptap/extension-list@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)) + '@tiptap/extension-paragraph': 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4)) + '@tiptap/extension-strike': 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4)) + '@tiptap/extension-text': 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4)) + '@tiptap/extension-underline': 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4)) + '@tiptap/extensions': 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4) + '@tiptap/pm': 3.20.4 + '@tootallnate/quickjs-emscripten@0.23.0': {} '@tsconfig/node10@1.0.12': {} @@ -5874,10 +6379,19 @@ snapshots: '@types/katex@0.16.8': {} + '@types/linkify-it@5.0.0': {} + + '@types/markdown-it@14.1.2': + dependencies: + '@types/linkify-it': 5.0.0 + '@types/mdurl': 2.0.0 + '@types/mdast@4.0.4': dependencies: '@types/unist': 3.0.3 + '@types/mdurl@2.0.0': {} + '@types/mocha@10.0.10': {} '@types/ms@2.1.0': {} @@ -5921,6 +6435,8 @@ snapshots: '@types/unist@3.0.3': {} + '@types/use-sync-external-store@0.0.6': {} + '@types/which@2.0.2': {} '@types/ws@8.18.1': @@ -7303,6 +7819,8 @@ snapshots: fast-deep-equal@3.1.3: {} + fast-equals@5.4.0: {} + fast-fifo@1.3.2: {} fast-json-stringify@6.3.0: @@ -7936,6 +8454,12 @@ snapshots: lines-and-columns@2.0.4: {} + linkify-it@5.0.0: + dependencies: + uc.micro: 2.1.0 + + linkifyjs@4.3.2: {} + listenercount@1.0.1: {} lit-element@4.2.2: @@ -8030,6 +8554,15 @@ snapshots: make-error@1.3.6: {} + markdown-it@14.1.1: + dependencies: + argparse: 2.0.1 + entities: 4.5.0 + linkify-it: 5.0.0 + mdurl: 2.0.0 + punycode.js: 2.3.1 + uc.micro: 2.1.0 + markdown-table@3.0.4: {} marked@16.4.2: {} @@ -8201,6 +8734,8 @@ snapshots: dependencies: '@types/mdast': 4.0.4 + mdurl@2.0.0: {} + mermaid@11.12.2: dependencies: '@braintree/sanitize-url': 7.1.2 @@ -8561,6 +9096,8 @@ snapshots: dependencies: wrappy: 1.0.2 + orderedmap@2.1.1: {} + p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 @@ -8769,6 +9306,109 @@ snapshots: property-information@7.1.0: {} + prosemirror-changeset@2.4.0: + dependencies: + prosemirror-transform: 1.11.0 + + prosemirror-collab@1.3.1: + dependencies: + prosemirror-state: 1.4.4 + + prosemirror-commands@1.7.1: + dependencies: + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.11.0 + + prosemirror-dropcursor@1.8.2: + dependencies: + prosemirror-state: 1.4.4 + prosemirror-transform: 1.11.0 + prosemirror-view: 1.41.7 + + prosemirror-gapcursor@1.4.1: + dependencies: + prosemirror-keymap: 1.2.3 + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-view: 1.41.7 + + prosemirror-history@1.5.0: + dependencies: + prosemirror-state: 1.4.4 + prosemirror-transform: 1.11.0 + prosemirror-view: 1.41.7 + rope-sequence: 1.3.4 + + prosemirror-inputrules@1.5.1: + dependencies: + prosemirror-state: 1.4.4 + prosemirror-transform: 1.11.0 + + prosemirror-keymap@1.2.3: + dependencies: + prosemirror-state: 1.4.4 + w3c-keyname: 2.2.8 + + prosemirror-markdown@1.13.4: + dependencies: + '@types/markdown-it': 14.1.2 + markdown-it: 14.1.1 + prosemirror-model: 1.25.4 + + prosemirror-menu@1.3.0: + dependencies: + crelt: 1.0.6 + prosemirror-commands: 1.7.1 + prosemirror-history: 1.5.0 + prosemirror-state: 1.4.4 + + prosemirror-model@1.25.4: + dependencies: + orderedmap: 2.1.1 + + prosemirror-schema-basic@1.2.4: + dependencies: + prosemirror-model: 1.25.4 + + prosemirror-schema-list@1.5.1: + dependencies: + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.11.0 + + prosemirror-state@1.4.4: + dependencies: + prosemirror-model: 1.25.4 + prosemirror-transform: 1.11.0 + prosemirror-view: 1.41.7 + + prosemirror-tables@1.8.5: + dependencies: + prosemirror-keymap: 1.2.3 + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.11.0 + prosemirror-view: 1.41.7 + + prosemirror-trailing-node@3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.7): + dependencies: + '@remirror/core-constants': 3.0.0 + escape-string-regexp: 4.0.0 + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-view: 1.41.7 + + prosemirror-transform@1.11.0: + dependencies: + prosemirror-model: 1.25.4 + + prosemirror-view@1.41.7: + dependencies: + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.11.0 + proxy-agent@6.3.1: dependencies: agent-base: 7.1.4 @@ -8802,6 +9442,8 @@ snapshots: end-of-stream: 1.4.5 once: 1.4.0 + punycode.js@2.3.1: {} + puppeteer-core@21.11.0: dependencies: '@puppeteer/browsers': 1.9.1 @@ -9085,6 +9727,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.57.1 fsevents: 2.3.3 + rope-sequence@1.3.4: {} + roughjs@4.6.6: dependencies: hachure-fill: 0.5.2 @@ -9467,6 +10111,8 @@ snapshots: ua-parser-js@1.0.41: {} + uc.micro@2.1.0: {} + ufo@1.6.3: {} unbzip2-stream@1.4.3: diff --git a/src/apps/desktop/Cargo.toml b/src/apps/desktop/Cargo.toml index bed49304..f521016a 100644 --- a/src/apps/desktop/Cargo.toml +++ b/src/apps/desktop/Cargo.toml @@ -43,6 +43,7 @@ ignore = { workspace = true } urlencoding = { workspace = true } reqwest = { workspace = true } thiserror = "1.0" +futures = { workspace = true } [target.'cfg(windows)'.dependencies] win32job = { workspace = true } diff --git a/src/apps/desktop/src/api/app_state.rs b/src/apps/desktop/src/api/app_state.rs index 4bf6d38c..cf6b3574 100644 --- a/src/apps/desktop/src/api/app_state.rs +++ b/src/apps/desktop/src/api/app_state.rs @@ -69,6 +69,7 @@ pub struct AppState { pub miniapp_manager: Arc, pub js_worker_pool: Option>, pub statistics: Arc>, + pub macos_edit_menu_mode: Arc>, pub start_time: std::time::Instant, // SSH Remote connection state pub ssh_manager: Arc>>, @@ -257,6 +258,9 @@ impl AppState { miniapp_manager, js_worker_pool, statistics, + macos_edit_menu_mode: Arc::new(RwLock::new( + crate::macos_menubar::EditMenuMode::System, + )), start_time, // SSH Remote connection state ssh_manager, diff --git a/src/apps/desktop/src/api/commands.rs b/src/apps/desktop/src/api/commands.rs index af94a82a..d2accfd3 100644 --- a/src/apps/desktop/src/api/commands.rs +++ b/src/apps/desktop/src/api/commands.rs @@ -204,10 +204,12 @@ async fn clear_active_workspace_context(state: &State<'_, AppState>, app: &AppHa .get_config::(Some("app.language")) .await .unwrap_or_else(|_| "zh-CN".to_string()); + let edit_mode = *state.macos_edit_menu_mode.read().await; let _ = crate::macos_menubar::set_macos_menubar_with_mode( app, &language, crate::macos_menubar::MenubarMode::Startup, + edit_mode, ); } } @@ -261,10 +263,12 @@ async fn apply_active_workspace_context( .get_config::(Some("app.language")) .await .unwrap_or_else(|_| "zh-CN".to_string()); + let edit_mode = *state.macos_edit_menu_mode.read().await; let _ = crate::macos_menubar::set_macos_menubar_with_mode( app, &language, crate::macos_menubar::MenubarMode::Workspace, + edit_mode, ); } } diff --git a/src/apps/desktop/src/api/editor_ai_api.rs b/src/apps/desktop/src/api/editor_ai_api.rs new file mode 100644 index 00000000..b9202378 --- /dev/null +++ b/src/apps/desktop/src/api/editor_ai_api.rs @@ -0,0 +1,198 @@ +//! Editor AI API +//! +//! Ephemeral streaming AI calls for in-editor experiences such as Markdown continuation: +//! - No session or dialog turn is created +//! - No persistence writes +//! - Supports streaming output and cancellation by request id + +use crate::api::app_state::AppState; +use bitfun_core::util::types::message::Message as AIMessage; +use futures::StreamExt; +use log::warn; +use serde::{Deserialize, Serialize}; +use tauri::{AppHandle, Emitter, State}; + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct EditorAiStreamRequest { + pub request_id: String, + pub prompt: String, + /// Optional model id override. Supports "fast"/"primary" aliases. + pub model_id: Option, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct EditorAiStreamResponse { + pub ok: bool, +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct EditorAiCancelRequest { + pub request_id: String, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct EditorAiTextChunkEvent { + pub request_id: String, + pub text: String, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct EditorAiCompletedEvent { + pub request_id: String, + pub full_text: String, + pub finish_reason: Option, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct EditorAiErrorEvent { + pub request_id: String, + pub error: String, +} + +fn system_prompt() -> &'static str { + "You are an in-editor AI writing assistant.\n\ +Follow the user's prompt exactly.\n\ +- Return only the requested document content.\n\ +- Do not add wrapper text or explanations unless the prompt explicitly asks for them.\n\ +- Do not call tools.\n" +} + +#[tauri::command] +pub async fn editor_ai_cancel( + state: State<'_, AppState>, + request: EditorAiCancelRequest, +) -> Result<(), String> { + if request.request_id.trim().is_empty() { + return Err("requestId is required".to_string()); + } + + state.side_question_runtime.cancel(&request.request_id).await; + Ok(()) +} + +#[tauri::command] +pub async fn editor_ai_stream( + app: AppHandle, + state: State<'_, AppState>, + request: EditorAiStreamRequest, +) -> Result { + if request.request_id.trim().is_empty() { + return Err("requestId is required".to_string()); + } + if request.prompt.trim().is_empty() { + return Err("prompt is required".to_string()); + } + + let model_id = request + .model_id + .as_deref() + .map(str::trim) + .filter(|value| !value.is_empty()) + .unwrap_or("primary") + .to_string(); + + let client = state + .ai_client_factory + .get_client_resolved(&model_id) + .await + .map_err(|error| format!("Failed to create AI client: {}", error))?; + + let cancel_token = state + .side_question_runtime + .register(request.request_id.clone()) + .await; + + let request_id = request.request_id.clone(); + let prompt = request.prompt.clone(); + let runtime = state.side_question_runtime.clone(); + let app_handle = app.clone(); + + tokio::spawn(async move { + let messages = vec![ + AIMessage::system(system_prompt().to_string()), + AIMessage::user(prompt), + ]; + + let mut full_text = String::new(); + let mut last_finish_reason: Option = None; + + let mut stream = match client.send_message_stream(messages, None).await { + Ok(response) => response.stream, + Err(error) => { + runtime.remove(&request_id).await; + let payload = EditorAiErrorEvent { + request_id, + error: format!("AI call failed: {}", error), + }; + if let Err(emit_error) = app_handle.emit("editor-ai://error", payload) { + warn!("Failed to emit editor AI error: {}", emit_error); + } + return; + } + }; + + while let Some(chunk_result) = stream.next().await { + if cancel_token.is_cancelled() { + runtime.remove(&request_id).await; + return; + } + + match chunk_result { + Ok(chunk) => { + if let Some(reason) = chunk.finish_reason.clone() { + last_finish_reason = Some(reason); + } + + if let Some(text) = chunk.text { + if text.is_empty() { + continue; + } + + full_text.push_str(&text); + let payload = EditorAiTextChunkEvent { + request_id: request_id.clone(), + text, + }; + if let Err(error) = app_handle.emit("editor-ai://text-chunk", payload) { + warn!("Failed to emit editor AI text chunk: {}", error); + } + } + } + Err(error) => { + runtime.remove(&request_id).await; + let payload = EditorAiErrorEvent { + request_id, + error: format!("Stream error: {}", error), + }; + if let Err(emit_error) = app_handle.emit("editor-ai://error", payload) { + warn!("Failed to emit editor AI error: {}", emit_error); + } + return; + } + } + } + + runtime.remove(&request_id).await; + + if cancel_token.is_cancelled() { + return; + } + + let payload = EditorAiCompletedEvent { + request_id, + full_text: full_text.trim().to_string(), + finish_reason: last_finish_reason, + }; + if let Err(error) = app_handle.emit("editor-ai://completed", payload) { + warn!("Failed to emit editor AI completion: {}", error); + } + }); + + Ok(EditorAiStreamResponse { ok: true }) +} diff --git a/src/apps/desktop/src/api/i18n_api.rs b/src/apps/desktop/src/api/i18n_api.rs index 98398842..13f08a93 100644 --- a/src/apps/desktop/src/api/i18n_api.rs +++ b/src/apps/desktop/src/api/i18n_api.rs @@ -68,10 +68,12 @@ pub async fn i18n_set_language( } else { crate::macos_menubar::MenubarMode::Startup }; + let edit_mode = *state.macos_edit_menu_mode.read().await; let _ = crate::macos_menubar::set_macos_menubar_with_mode( &_app, &request.language, mode, + edit_mode, ); } Ok(format!("Language switched to: {}", request.language)) diff --git a/src/apps/desktop/src/api/mod.rs b/src/apps/desktop/src/api/mod.rs index 9a60278e..be8d7a43 100644 --- a/src/apps/desktop/src/api/mod.rs +++ b/src/apps/desktop/src/api/mod.rs @@ -13,6 +13,7 @@ pub mod context_upload_api; pub mod cron_api; pub mod diff_api; pub mod dto; +pub mod editor_ai_api; pub mod git_agent_api; pub mod git_api; pub mod i18n_api; diff --git a/src/apps/desktop/src/api/system_api.rs b/src/apps/desktop/src/api/system_api.rs index a6ae09b3..7e4a694c 100644 --- a/src/apps/desktop/src/api/system_api.rs +++ b/src/apps/desktop/src/api/system_api.rs @@ -1,7 +1,9 @@ //! System API +use crate::api::app_state::AppState; use bitfun_core::service::system; use serde::{Deserialize, Serialize}; +use tauri::State; #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -55,6 +57,12 @@ pub struct CommandOutputResponse { pub success: bool, } +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SetMacosEditMenuModeRequest { + pub mode: crate::macos_menubar::EditMenuMode, +} + #[tauri::command] pub async fn check_command_exists(command: String) -> Result { let result = system::check_command(&command); @@ -112,3 +120,49 @@ pub async fn run_system_command( success: result.success, }) } + +#[tauri::command] +pub async fn set_macos_edit_menu_mode( + state: State<'_, AppState>, + app: tauri::AppHandle, + request: SetMacosEditMenuModeRequest, +) -> Result<(), String> { + #[cfg(target_os = "macos")] + { + let current_mode = *state.macos_edit_menu_mode.read().await; + if current_mode == request.mode { + return Ok(()); + } + + { + let mut edit_mode = state.macos_edit_menu_mode.write().await; + *edit_mode = request.mode; + } + + let language = state + .config_service + .get_config::(Some("app.language")) + .await + .unwrap_or_else(|_| "zh-CN".to_string()); + let menubar_mode = if state.workspace_path.read().await.is_some() { + crate::macos_menubar::MenubarMode::Workspace + } else { + crate::macos_menubar::MenubarMode::Startup + }; + + crate::macos_menubar::set_macos_menubar_with_mode( + &app, + &language, + menubar_mode, + request.mode, + ) + .map_err(|error| error.to_string())?; + } + + #[cfg(not(target_os = "macos"))] + { + let _ = (&state, &app, &request); + } + + Ok(()) +} diff --git a/src/apps/desktop/src/lib.rs b/src/apps/desktop/src/lib.rs index e1a6303b..e1341270 100644 --- a/src/apps/desktop/src/lib.rs +++ b/src/apps/desktop/src/lib.rs @@ -145,15 +145,8 @@ pub async fn run() { #[cfg(target_os = "macos")] { app.on_menu_event(|app, event| { - let event_name = if event.id() == "bitfun.open_project" { - Some("bitfun_menu_open_project") - } else if event.id() == "bitfun.new_project" { - Some("bitfun_menu_new_project") - } else if event.id() == "bitfun.about" { - Some("bitfun_menu_about") - } else { - None - }; + let event_name = + crate::macos_menubar::menu_event_name_for_id(event.id().as_ref()); if let Some(event_name) = event_name { let _ = app.emit(event_name, ()); @@ -214,6 +207,7 @@ pub async fn run() { let app_state: tauri::State<'_, api::app_state::AppState> = app.state(); let config_service = app_state.config_service.clone(); let workspace_path = app_state.workspace_path.clone(); + let macos_edit_menu_mode = app_state.macos_edit_menu_mode.clone(); tokio::spawn(async move { let language = config_service @@ -227,11 +221,13 @@ pub async fn run() { } else { crate::macos_menubar::MenubarMode::Startup }; + let edit_mode = *macos_edit_menu_mode.read().await; let _ = crate::macos_menubar::set_macos_menubar_with_mode( &app_handle_for_menu, &language, mode, + edit_mode, ); }); } @@ -307,6 +303,8 @@ pub async fn run() { api::btw_api::btw_ask, api::btw_api::btw_ask_stream, api::btw_api::btw_cancel, + api::editor_ai_api::editor_ai_stream, + api::editor_ai_api::editor_ai_cancel, api::context_upload_api::upload_image_contexts, get_all_tools_info, get_readonly_tools_info, @@ -581,6 +579,7 @@ pub async fn run() { check_command_exists, check_commands_exist, run_system_command, + set_macos_edit_menu_mode, i18n_get_current_language, i18n_set_language, i18n_get_supported_languages, diff --git a/src/apps/desktop/src/macos_menubar.rs b/src/apps/desktop/src/macos_menubar.rs index d348a967..59d5a654 100644 --- a/src/apps/desktop/src/macos_menubar.rs +++ b/src/apps/desktop/src/macos_menubar.rs @@ -1,7 +1,7 @@ //! macOS Native Menubar #[cfg(target_os = "macos")] -use tauri::menu::{MenuBuilder, SubmenuBuilder}; +use tauri::menu::{MenuBuilder, MenuItemBuilder, SubmenuBuilder}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum MenubarMode { @@ -9,6 +9,35 @@ pub enum MenubarMode { Workspace, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum EditMenuMode { + System, + Renderer, +} + +pub const MENU_ID_EDIT_UNDO: &str = "bitfun.edit.undo"; +pub const MENU_ID_EDIT_REDO: &str = "bitfun.edit.redo"; +pub const MENU_ID_EDIT_CUT: &str = "bitfun.edit.cut"; +pub const MENU_ID_EDIT_COPY: &str = "bitfun.edit.copy"; +pub const MENU_ID_EDIT_PASTE: &str = "bitfun.edit.paste"; +pub const MENU_ID_EDIT_SELECT_ALL: &str = "bitfun.edit.select_all"; + +pub fn menu_event_name_for_id(id: &str) -> Option<&'static str> { + match id { + "bitfun.open_project" => Some("bitfun_menu_open_project"), + "bitfun.new_project" => Some("bitfun_menu_new_project"), + "bitfun.about" => Some("bitfun_menu_about"), + MENU_ID_EDIT_UNDO => Some("bitfun_menu_edit_undo"), + MENU_ID_EDIT_REDO => Some("bitfun_menu_edit_redo"), + MENU_ID_EDIT_CUT => Some("bitfun_menu_edit_cut"), + MENU_ID_EDIT_COPY => Some("bitfun_menu_edit_copy"), + MENU_ID_EDIT_PASTE => Some("bitfun_menu_edit_paste"), + MENU_ID_EDIT_SELECT_ALL => Some("bitfun_menu_edit_select_all"), + _ => None, + } +} + #[cfg(target_os = "macos")] #[derive(Clone)] struct MenubarLabels { @@ -17,6 +46,12 @@ struct MenubarLabels { open_project: &'static str, new_project: &'static str, about_bitfun: &'static str, + undo: &'static str, + redo: &'static str, + cut: &'static str, + copy: &'static str, + paste: &'static str, + select_all: &'static str, } #[cfg(target_os = "macos")] @@ -28,6 +63,12 @@ fn labels_for_language(language: &str) -> MenubarLabels { open_project: "Open Project…", new_project: "New Project…", about_bitfun: "About BitFun", + undo: "Undo", + redo: "Redo", + cut: "Cut", + copy: "Copy", + paste: "Paste", + select_all: "Select All", }, _ => MenubarLabels { project_menu: "工程", @@ -35,6 +76,12 @@ fn labels_for_language(language: &str) -> MenubarLabels { open_project: "打开工程…", new_project: "新建工程…", about_bitfun: "关于 BitFun", + undo: "撤销", + redo: "重做", + cut: "剪切", + copy: "复制", + paste: "粘贴", + select_all: "全选", }, } } @@ -44,68 +91,70 @@ pub fn set_macos_menubar_with_mode( app: &tauri::AppHandle, language: &str, mode: MenubarMode, + edit_mode: EditMenuMode, ) -> tauri::Result<()> { let labels = labels_for_language(language); + let _ = mode; - let menu = match mode { - MenubarMode::Startup => { - let app_menu = SubmenuBuilder::new(app, "BitFun") - .text("bitfun.about", labels.about_bitfun) - .separator() - .quit() - .build()?; - - let edit_menu = SubmenuBuilder::new(app, labels.edit_menu) - .undo() - .redo() - .separator() - .cut() - .copy() - .paste() - .select_all() - .build()?; + let app_menu = SubmenuBuilder::new(app, "BitFun") + .text("bitfun.about", labels.about_bitfun) + .separator() + .quit() + .build()?; - let project_menu = SubmenuBuilder::new(app, labels.project_menu) - .text("bitfun.open_project", labels.open_project) - .text("bitfun.new_project", labels.new_project) - .build()?; + let edit_menu = match edit_mode { + EditMenuMode::System => SubmenuBuilder::new(app, labels.edit_menu) + .undo() + .redo() + .separator() + .cut() + .copy() + .paste() + .select_all() + .build()?, + EditMenuMode::Renderer => { + let undo = MenuItemBuilder::with_id(MENU_ID_EDIT_UNDO, labels.undo) + .accelerator("Cmd+Z") + .build(app)?; + let redo = MenuItemBuilder::with_id(MENU_ID_EDIT_REDO, labels.redo) + .accelerator("Cmd+Shift+Z") + .build(app)?; + let cut = MenuItemBuilder::with_id(MENU_ID_EDIT_CUT, labels.cut) + .accelerator("Cmd+X") + .build(app)?; + let copy = MenuItemBuilder::with_id(MENU_ID_EDIT_COPY, labels.copy) + .accelerator("Cmd+C") + .build(app)?; + let paste = MenuItemBuilder::with_id(MENU_ID_EDIT_PASTE, labels.paste) + .accelerator("Cmd+V") + .build(app)?; + let select_all = MenuItemBuilder::with_id(MENU_ID_EDIT_SELECT_ALL, labels.select_all) + .accelerator("Cmd+A") + .build(app)?; - MenuBuilder::new(app) - .item(&app_menu) - .item(&edit_menu) - .item(&project_menu) - .build()? - } - MenubarMode::Workspace => { - let app_menu = SubmenuBuilder::new(app, "BitFun") - .text("bitfun.about", labels.about_bitfun) - .separator() - .quit() - .build()?; - - let edit_menu = SubmenuBuilder::new(app, labels.edit_menu) - .undo() - .redo() + SubmenuBuilder::new(app, labels.edit_menu) + .item(&undo) + .item(&redo) .separator() - .cut() - .copy() - .paste() - .select_all() - .build()?; - - let project_menu = SubmenuBuilder::new(app, labels.project_menu) - .text("bitfun.open_project", labels.open_project) - .text("bitfun.new_project", labels.new_project) - .build()?; - - MenuBuilder::new(app) - .item(&app_menu) - .item(&edit_menu) - .item(&project_menu) + .item(&cut) + .item(©) + .item(&paste) + .item(&select_all) .build()? } }; + let project_menu = SubmenuBuilder::new(app, labels.project_menu) + .text("bitfun.open_project", labels.open_project) + .text("bitfun.new_project", labels.new_project) + .build()?; + + let menu = MenuBuilder::new(app) + .item(&app_menu) + .item(&edit_menu) + .item(&project_menu) + .build()?; + app.set_menu(menu)?; Ok(()) } @@ -115,6 +164,7 @@ pub fn set_macos_menubar_with_mode( _app: &tauri::AppHandle, _language: &str, _mode: MenubarMode, + _edit_mode: EditMenuMode, ) -> tauri::Result<()> { Ok(()) } diff --git a/src/web-ui/package.json b/src/web-ui/package.json index 7451efc1..5ffbb0a9 100644 --- a/src/web-ui/package.json +++ b/src/web-ui/package.json @@ -21,6 +21,14 @@ "@tauri-apps/plugin-fs": "^2.0.0", "@tauri-apps/plugin-log": "^2.8.0", "@tauri-apps/plugin-opener": "^2.5.2", + "@tiptap/core": "^3.20.4", + "@tiptap/extension-link": "^3.20.4", + "@tiptap/extension-placeholder": "^3.20.4", + "@tiptap/extension-task-item": "^3.20.4", + "@tiptap/extension-task-list": "^3.20.4", + "@tiptap/pm": "^3.20.4", + "@tiptap/react": "^3.20.4", + "@tiptap/starter-kit": "^3.20.4", "@xterm/addon-fit": "^0.12.0-beta.103", "@xterm/addon-web-links": "^0.13.0-beta.103", "@xterm/addon-webgl": "^0.20.0-beta.100", diff --git a/src/web-ui/src/app/components/NavPanel/components/PersistentFooterActions.tsx b/src/web-ui/src/app/components/NavPanel/components/PersistentFooterActions.tsx index 3e7f7b70..f0501e23 100644 --- a/src/web-ui/src/app/components/NavPanel/components/PersistentFooterActions.tsx +++ b/src/web-ui/src/app/components/NavPanel/components/PersistentFooterActions.tsx @@ -1,5 +1,5 @@ import React, { useState, useCallback } from 'react'; -import { Settings, Info, MoreVertical, PictureInPicture2, SquareTerminal, Smartphone, Globe } from 'lucide-react'; +import { Settings, Info, MoreVertical, PictureInPicture2, SquareTerminal, Wifi, Globe } from 'lucide-react'; import { Tooltip, Modal } from '@/component-library'; import { useI18n } from '@/infrastructure/i18n/hooks/useI18n'; import { useSceneManager } from '../../../hooks/useSceneManager'; @@ -125,126 +125,126 @@ const PersistentFooterActions: React.FC = () => { return ( <> -
-
-
- - - - - {menuOpen && ( - <> -
-
+
+
+ + + + + {menuOpen && ( + <> +
+
+ + + +
- -
- -
- - -
- - )} -
+
+ + +
+ + )} +
- - - - - - - -
+ + + -
- + + + +
+ +
+ +
-
- setShowAbout(false)} /> - setShowRemoteConnect(false)} /> - setShowRemoteDisclaimer(false)} - title={t('remoteConnect.disclaimerTitle')} - showCloseButton - size="large" - contentInset - > - setShowAbout(false)} /> + setShowRemoteConnect(false)} /> + setShowRemoteDisclaimer(false)} - onAgree={handleAgreeDisclaimer} - /> - + title={t('remoteConnect.disclaimerTitle')} + showCloseButton + size="large" + contentInset + > + setShowRemoteDisclaimer(false)} + onAgree={handleAgreeDisclaimer} + /> + ); }; diff --git a/src/web-ui/src/infrastructure/api/index.ts b/src/web-ui/src/infrastructure/api/index.ts index a41475e7..d7b31700 100644 --- a/src/web-ui/src/infrastructure/api/index.ts +++ b/src/web-ui/src/infrastructure/api/index.ts @@ -30,11 +30,12 @@ import { startchatAgentAPI } from './service-api/StartchatAgentAPI'; import { sessionAPI } from './service-api/SessionAPI'; import { i18nAPI } from './service-api/I18nAPI'; import { btwAPI } from './service-api/BtwAPI'; +import { editorAiAPI } from './service-api/EditorAiAPI'; import { tokenUsageApi } from './tokenUsageApi'; import { insightsApi } from './insightsApi'; // Export API modules -export { workspaceAPI, configAPI, aiApi, toolAPI, agentAPI, systemAPI, projectAPI, diffAPI, snapshotAPI, globalAPI, contextAPI, cronAPI, gitAPI, gitAgentAPI, gitRepoHistoryAPI, startchatAgentAPI, sessionAPI, i18nAPI, btwAPI, tokenUsageApi, insightsApi }; +export { workspaceAPI, configAPI, aiApi, toolAPI, agentAPI, systemAPI, projectAPI, diffAPI, snapshotAPI, globalAPI, contextAPI, cronAPI, gitAPI, gitAgentAPI, gitRepoHistoryAPI, startchatAgentAPI, sessionAPI, i18nAPI, btwAPI, editorAiAPI, tokenUsageApi, insightsApi }; // Export types export type { GitRepoHistory }; @@ -60,6 +61,7 @@ export const bitfunAPI = { session: sessionAPI, i18n: i18nAPI, btw: btwAPI, + editorAi: editorAiAPI, tokenUsage: tokenUsageApi, insights: insightsApi, }; diff --git a/src/web-ui/src/infrastructure/api/service-api/EditorAiAPI.ts b/src/web-ui/src/infrastructure/api/service-api/EditorAiAPI.ts new file mode 100644 index 00000000..3e5e01ff --- /dev/null +++ b/src/web-ui/src/infrastructure/api/service-api/EditorAiAPI.ts @@ -0,0 +1,64 @@ +import { api } from './ApiClient'; +import { createTauriCommandError } from '../errors/TauriCommandError'; + +export interface EditorAiStreamRequest { + requestId: string; + prompt: string; + modelId?: string; +} + +export interface EditorAiStreamResponse { + ok: boolean; +} + +export interface EditorAiCancelRequest { + requestId: string; +} + +export interface EditorAiTextChunkEvent { + requestId: string; + text: string; +} + +export interface EditorAiCompletedEvent { + requestId: string; + fullText: string; + finishReason?: string | null; +} + +export interface EditorAiErrorEvent { + requestId: string; + error: string; +} + +export class EditorAiAPI { + async stream(request: EditorAiStreamRequest): Promise { + try { + return await api.invoke('editor_ai_stream', { request }); + } catch (error) { + throw createTauriCommandError('editor_ai_stream', error, request); + } + } + + async cancel(request: EditorAiCancelRequest): Promise { + try { + await api.invoke('editor_ai_cancel', { request }); + } catch (error) { + throw createTauriCommandError('editor_ai_cancel', error, request); + } + } + + onTextChunk(callback: (event: EditorAiTextChunkEvent) => void): () => void { + return api.listen('editor-ai://text-chunk', callback); + } + + onCompleted(callback: (event: EditorAiCompletedEvent) => void): () => void { + return api.listen('editor-ai://completed', callback); + } + + onError(callback: (event: EditorAiErrorEvent) => void): () => void { + return api.listen('editor-ai://error', callback); + } +} + +export const editorAiAPI = new EditorAiAPI(); diff --git a/src/web-ui/src/infrastructure/api/service-api/SystemAPI.ts b/src/web-ui/src/infrastructure/api/service-api/SystemAPI.ts index 84f2d5d5..0c7c68d7 100644 --- a/src/web-ui/src/infrastructure/api/service-api/SystemAPI.ts +++ b/src/web-ui/src/infrastructure/api/service-api/SystemAPI.ts @@ -113,7 +113,17 @@ export class SystemAPI { throw createTauriCommandError('check_commands_exist', error, { commands }); } } + + async setMacosEditMenuMode(mode: 'system' | 'renderer'): Promise { + try { + await api.invoke('set_macos_edit_menu_mode', { + request: { mode } + }); + } catch (error) { + throw createTauriCommandError('set_macos_edit_menu_mode', error, { mode }); + } + } } -export const systemAPI = new SystemAPI(); \ No newline at end of file +export const systemAPI = new SystemAPI(); diff --git a/src/web-ui/src/locales/en-US/tools.json b/src/web-ui/src/locales/en-US/tools.json index 38f0c986..fadda364 100644 --- a/src/web-ui/src/locales/en-US/tools.json +++ b/src/web-ui/src/locales/en-US/tools.json @@ -102,6 +102,38 @@ "localImageLoading": "Loading image...", "localImageFailed": "Image failed to load", "insertLinkTextPlaceholder": "Link text", + "inlineAi": { + "triggerHint": "Press Space to use AI", + "askMode": "Ask AI", + "continueMode": "Continue Writing", + "askPlaceholder": "Ask AI anything...", + "continuePlaceholder": "Add a direction for the continuation...", + "currentPage": "Current Page", + "suggestionSection": "Suggestions", + "summaryAction": "Add Summary", + "todoAction": "Add Todo List", + "summaryDirection": "Insert a short summary at the current position that captures the key points above while keeping the current document language and style.", + "todoDirection": "Insert a concise Markdown todo list at the current position that extracts the next action items while keeping the current document language and style.", + "thinkSection": "Think, Ask, Discuss", + "cancel": "Cancel", + "working": "Working...", + "askSubmit": "Send to AI", + "continueSubmit": "Continue", + "accept": "Accept", + "reject": "Discard", + "previewTitle": "AI Preview", + "previewStreaming": "Streaming", + "previewReady": "Ready to insert", + "workspaceRequired": "Please open a workspace before using AI assist.", + "askEmpty": "Please enter a question for AI.", + "askSent": "Question sent to the AI workspace session.", + "askFailed": "Failed to send question.", + "continueInserted": "AI continuation inserted.", + "continueEmptyResult": "AI returned empty content or the insertion point is no longer available.", + "continueFailed": "AI continuation failed.", + "continueStartFailed": "Failed to start AI continuation.", + "tempSessionName": "MEditor Inline Continue" + }, "shortcuts": { "undo": "Undo", "redo": "Redo", diff --git a/src/web-ui/src/locales/zh-CN/tools.json b/src/web-ui/src/locales/zh-CN/tools.json index 13fd6bfc..fa5162ad 100644 --- a/src/web-ui/src/locales/zh-CN/tools.json +++ b/src/web-ui/src/locales/zh-CN/tools.json @@ -102,6 +102,38 @@ "localImageLoading": "图片加载中...", "localImageFailed": "图片加载失败", "insertLinkTextPlaceholder": "链接文本", + "inlineAi": { + "triggerHint": "按空格以启用 AI", + "askMode": "问 AI", + "continueMode": "继续编写", + "askPlaceholder": "万事问 AI ...", + "continuePlaceholder": "补充一下你希望续写的方向...", + "currentPage": "当前页面", + "suggestionSection": "建议", + "summaryAction": "添加摘要", + "todoAction": "添加待办事项", + "summaryDirection": "在当前位置插入一段简短摘要,概括上文的关键内容,并保持当前文档语言和风格。", + "todoDirection": "在当前位置插入一个简洁的 Markdown 待办事项列表,提炼出接下来的行动项,并保持当前文档语言和风格。", + "thinkSection": "思考、询问、对话", + "cancel": "取消", + "working": "处理中...", + "askSubmit": "发送给 AI", + "continueSubmit": "继续编写", + "accept": "接受写入", + "reject": "丢弃", + "previewTitle": "AI 预览", + "previewStreaming": "生成中", + "previewReady": "待确认", + "workspaceRequired": "请先打开工作区,再使用 AI 辅助。", + "askEmpty": "请输入要问 AI 的内容。", + "askSent": "问题已发送到当前工作区的 AI 会话。", + "askFailed": "发送问题失败。", + "continueInserted": "AI 续写内容已插入。", + "continueEmptyResult": "AI 没有返回可插入内容,或者插入位置已失效。", + "continueFailed": "AI 续写失败。", + "continueStartFailed": "启动 AI 续写失败。", + "tempSessionName": "MEditor 内联续写" + }, "shortcuts": { "undo": "撤销", "redo": "重做", diff --git a/src/web-ui/src/tools/editor/components/CodeEditor.tsx b/src/web-ui/src/tools/editor/components/CodeEditor.tsx index 04dd37a7..50f5e9d6 100644 --- a/src/web-ui/src/tools/editor/components/CodeEditor.tsx +++ b/src/web-ui/src/tools/editor/components/CodeEditor.tsx @@ -10,6 +10,7 @@ import { AlertCircle } from 'lucide-react'; import * as monaco from 'monaco-editor'; import { monacoInitManager } from '../services/MonacoInitManager'; import { monacoModelManager } from '../services/MonacoModelManager'; +import { activeEditTargetService, createMonacoEditTarget } from '../services/ActiveEditTargetService'; import { forceRegisterTheme, BitFunDarkTheme, @@ -92,6 +93,15 @@ function hasVeryLongLine(content: string, maxLineLength: number): boolean { return false; } +function isMacOSDesktop(): boolean { + if (typeof window === 'undefined') { + return false; + } + + const isTauri = '__TAURI__' in window; + return isTauri && typeof navigator.platform === 'string' && navigator.platform.toUpperCase().includes('MAC'); +} + const CodeEditor: React.FC = ({ filePath: rawFilePath, workspacePath, @@ -202,6 +212,8 @@ const CodeEditor: React.FC = ({ const userIndentRef = useRef<{ tab_size: number; insert_spaces: boolean } | null>(null); const largeFileModeRef = useRef(false); const largeFileExpansionBlockedLogRef = useRef(false); + const pendingModelContentRef = useRef(null); + const macosEditorBindingCleanupRef = useRef<(() => void) | null>(null); const detectLargeFileMode = useCallback((nextContent: string, fileSizeBytes?: number): boolean => { const size = typeof fileSizeBytes === 'number' && fileSizeBytes >= 0 @@ -226,6 +238,29 @@ const CodeEditor: React.FC = ({ } }, [detectLargeFileMode, filePath]); + const applyExternalContentToModel = useCallback((nextContent: string) => { + const model = modelRef.current; + if (!model) { + pendingModelContentRef.current = nextContent; + return; + } + + pendingModelContentRef.current = null; + if (model.getValue() === nextContent) { + return; + } + + const previousLoadingState = isLoadingContentRef.current; + isLoadingContentRef.current = true; + model.setValue(nextContent); + + queueMicrotask(() => { + if (!isUnmountedRef.current) { + isLoadingContentRef.current = previousLoadingState; + } + }); + }, []); + const shouldBlockLargeFileExpansionClick = useCallback((target: EventTarget | null): boolean => { if (!(target instanceof HTMLElement)) { return false; @@ -250,6 +285,7 @@ const CodeEditor: React.FC = ({ useEffect(() => { filePathRef.current = filePath; + pendingModelContentRef.current = null; }, [filePath]); useEffect(() => { @@ -580,6 +616,25 @@ const CodeEditor: React.FC = ({ editor = monaco.editor.create(containerRef.current, editorOptions); editorRef.current = editor; setEditorInstance(editor); + const editTarget = createMonacoEditTarget(editor); + const unbindEditTarget = activeEditTargetService.bindTarget(editTarget); + const focusDisposable = editor.onDidFocusEditorText(() => { + activeEditTargetService.setActiveTarget(editTarget.id); + }); + const blurDisposable = editor.onDidBlurEditorText(() => { + window.setTimeout(() => { + if (editor?.hasTextFocus()) { + return; + } + + activeEditTargetService.clearActiveTarget(editTarget.id); + }, 0); + }); + macosEditorBindingCleanupRef.current = () => { + focusDisposable.dispose(); + blurDisposable.dispose(); + unbindEditTarget(); + }; // #endregion (containerRef.current as any).__monacoEditor = editor; @@ -794,6 +849,10 @@ const CodeEditor: React.FC = ({ return () => { isUnmountedRef.current = true; + if (macosEditorBindingCleanupRef.current) { + macosEditorBindingCleanupRef.current(); + macosEditorBindingCleanupRef.current = null; + } if (delayedFontApplyTimerRef.current) { clearTimeout(delayedFontApplyTimerRef.current); delayedFontApplyTimerRef.current = null; @@ -836,26 +895,16 @@ const CodeEditor: React.FC = ({ }, [filePath, detectedLanguage, detectLargeFileMode]); useEffect(() => { - if (modelRef.current && monacoReady && !loading) { - const currentValue = modelRef.current.getValue(); - if (content !== currentValue) { - isLoadingContentRef.current = true; - - monacoModelManager.updateModelContent(filePath, content, !hasChanges); - - queueMicrotask(() => { - isLoadingContentRef.current = false; - if (modelRef.current && !isUnmountedRef.current && !hasChanges) { - savedVersionIdRef.current = modelRef.current.getAlternativeVersionId(); - } - }); - - if (content && !lspReady) { - setLspReady(true); - } - } + if (monacoReady && pendingModelContentRef.current !== null) { + applyExternalContentToModel(pendingModelContentRef.current); } - }, [content, monacoReady, loading, lspReady, hasChanges, filePath]); + }, [monacoReady, applyExternalContentToModel]); + + useEffect(() => { + if (content && !lspReady) { + setLspReady(true); + } + }, [content, lspReady]); useEffect(() => { if (modelRef.current && monacoReady) { @@ -1029,13 +1078,19 @@ const CodeEditor: React.FC = ({ updateLargeFileMode(content); setContent(content); originalContentRef.current = content; - if (modelRef.current) { - modelRef.current.setValue(content); - } + setHasChanges(false); + hasChangesRef.current = false; + applyExternalContentToModel(content); + queueMicrotask(() => { + if (modelRef.current && !isUnmountedRef.current) { + savedVersionIdRef.current = modelRef.current.getAlternativeVersionId(); + monacoModelManager.markAsSaved(filePath); + } + }); } catch (err) { log.warn('Failed to reload file with new encoding', err); } - }, [filePath, updateLargeFileMode]); + }, [applyExternalContentToModel, filePath, updateLargeFileMode]); const handleLanguageConfirm = useCallback((languageId: string) => { userLanguageOverrideRef.current = true; @@ -1087,6 +1142,7 @@ const CodeEditor: React.FC = ({ originalContentRef.current = fileContent; setHasChanges(false); hasChangesRef.current = false; + applyExternalContentToModel(fileContent); // NOTE: Do NOT call onContentChange here during initial load. // Calling it triggers parent re-render which unmounts this component, @@ -1120,7 +1176,7 @@ const CodeEditor: React.FC = ({ isLoadingContentRef.current = false; }); } - }, [filePath, detectedLanguage, t, updateLargeFileMode]); + }, [applyExternalContentToModel, filePath, detectedLanguage, t, updateLargeFileMode]); // Save file content const saveFileContent = useCallback(async () => { @@ -1185,19 +1241,43 @@ const CodeEditor: React.FC = ({ // Container-level keyboard event handler, solves global conflict issues with multiple editor instances const handleContainerKeyDown = useCallback((event: React.KeyboardEvent) => { - if ((event.ctrlKey || event.metaKey) && event.key === 's') { - // Check if editor has focus - const hasFocus = editorRef.current?.hasTextFocus() ?? false; - - if (hasFocus) { - event.preventDefault(); - event.stopPropagation(); - - saveFileContentRef.current?.(); + const hasFocus = editorRef.current?.hasTextFocus() ?? false; + if (!hasFocus) { + return; + } + + const isModKey = event.ctrlKey || event.metaKey; + const lowerKey = event.key.toLowerCase(); + + if (isModKey && lowerKey === 's') { + event.preventDefault(); + event.stopPropagation(); + saveFileContentRef.current?.(); + return; + } + + if (isModKey && lowerKey === 'z') { + if (isMacOSDesktop()) { + return; } - // If no focus, don't prevent event propagation, let other editors or parent components handle it + + event.preventDefault(); + event.stopPropagation(); + + if (event.shiftKey) { + activeEditTargetService.executeAction('redo'); + } else { + activeEditTargetService.executeAction('undo'); + } + return; } - }, [filePath]); + + if (!event.metaKey && event.ctrlKey && lowerKey === 'y') { + event.preventDefault(); + event.stopPropagation(); + activeEditTargetService.executeAction('redo'); + } + }, []); // Check file modifications const checkFileModification = useCallback(async () => { @@ -1237,6 +1317,7 @@ const CodeEditor: React.FC = ({ setHasChanges(false); hasChangesRef.current = false; lastModifiedTimeRef.current = currentModifiedTime; + applyExternalContentToModel(fileContent); onContentChange?.(fileContent, false); @@ -1256,7 +1337,7 @@ const CodeEditor: React.FC = ({ } finally { isCheckingFileRef.current = false; } - }, [filePath, hasChanges, monacoReady, updateLargeFileMode]); + }, [applyExternalContentToModel, filePath, hasChanges, monacoReady, onContentChange, t, updateLargeFileMode]); // Initial file load - only run once when filePath changes const loadFileContentCalledRef = useRef(false); @@ -1466,13 +1547,22 @@ const CodeEditor: React.FC = ({ const currentPosition = editor?.getPosition(); - if (editor) { - editor.setValue(content); - - if (currentPosition) { - editor.setPosition(currentPosition); - } + setContent(content); + originalContentRef.current = content; + setHasChanges(false); + hasChangesRef.current = false; + applyExternalContentToModel(content); + + if (editor && currentPosition) { + editor.setPosition(currentPosition); } + + queueMicrotask(() => { + if (modelRef.current && !isUnmountedRef.current) { + savedVersionIdRef.current = modelRef.current.getAlternativeVersionId(); + monacoModelManager.markAsSaved(filePath); + } + }); } catch (error) { log.error('Failed to reload file', error); } @@ -1494,7 +1584,7 @@ const CodeEditor: React.FC = ({ return () => { unsubscribers.forEach(unsub => unsub()); }; - }, [monacoReady, filePath, updateLargeFileMode]); + }, [applyExternalContentToModel, monacoReady, filePath, updateLargeFileMode]); useEffect(() => { userLanguageOverrideRef.current = false; diff --git a/src/web-ui/src/tools/editor/components/MarkdownEditor.scss b/src/web-ui/src/tools/editor/components/MarkdownEditor.scss index b392b0c8..ce3e3680 100644 --- a/src/web-ui/src/tools/editor/components/MarkdownEditor.scss +++ b/src/web-ui/src/tools/editor/components/MarkdownEditor.scss @@ -82,6 +82,11 @@ // Markdown content styles .markdown-body { + --m-editor-list-indent: 1.75rem; + --m-editor-list-marker-width: 1.25rem; + --m-editor-list-gap: 0.25rem; + --m-editor-task-checkbox-size: 1rem; + color: var(--color-text-secondary); font-family: $font-family-sans; font-size: $font-size-sm; @@ -102,7 +107,8 @@ h1 { font-size: $font-size-3xl; - border-bottom: 1px solid rgba(255, 255, 255, 0.05); + color: #111111; + border-bottom: 1px solid rgba(17, 17, 17, 0.12); padding-bottom: $size-gap-3; text-align: center; letter-spacing: 0.5px; @@ -184,17 +190,66 @@ } } - ul, ol { - padding-left: 2em; + > ul:not(.contains-task-list), + > ol { + padding-left: 0; margin-top: 0; margin-bottom: $size-gap-4; } + > ul:not(.contains-task-list) > li, + > ol > li { + list-style: none; + position: relative; + margin-bottom: $size-gap-1; + padding-left: calc(var(--m-editor-list-marker-width) + var(--m-editor-list-gap)); + } + + > ul:not(.contains-task-list) > li::before, + > ol > li::before { + position: absolute; + left: 0; + top: 0; + width: var(--m-editor-list-marker-width); + color: var(--color-text-primary); + font-weight: $font-weight-medium; + } + + > ul:not(.contains-task-list) > li::before { + content: '•'; + text-align: center; + } + + > ol { + counter-reset: m-editor-ordered-list; + } + + > ol > li { + counter-increment: m-editor-ordered-list; + } + + > ol > li::before { + content: counter(m-editor-ordered-list) '.'; + text-align: left; + } + + li > ul:not(.contains-task-list), + li > ol { + margin-top: $size-gap-1; + margin-bottom: 0; + padding-left: var(--m-editor-list-indent); + } + li { margin-bottom: $size-gap-1; > p { - margin-top: $size-gap-4; + margin: 0; + display: block; + } + + > p + p { + margin-top: $size-gap-2; } } @@ -259,9 +314,34 @@ } // Task list - input[type="checkbox"] { - margin: 0 0.2em 0.25em -1.6em; - vertical-align: middle; + > ul.contains-task-list { + margin-top: 0; + margin-bottom: $size-gap-4; + padding-left: 0; + list-style: none; + } + + > ul.contains-task-list > .task-list-item { + list-style: none; + position: relative; + padding-left: calc(var(--m-editor-list-marker-width) + var(--m-editor-list-gap)); + margin-bottom: $size-gap-1; + } + + > ul.contains-task-list > .task-list-item > input[type="checkbox"] { + position: absolute; + left: calc((var(--m-editor-list-marker-width) - var(--m-editor-task-checkbox-size)) / 2); + top: 0.15rem; + margin: 0; + width: var(--m-editor-task-checkbox-size); + height: var(--m-editor-task-checkbox-size); + } + + .task-list-item > ul, + .task-list-item > ol { + margin-top: $size-gap-1; + margin-bottom: 0; + padding-left: var(--m-editor-list-indent); } // KaTeX math formula styles @@ -308,4 +388,3 @@ text-align: center; } } - diff --git a/src/web-ui/src/tools/editor/components/MarkdownEditor.tsx b/src/web-ui/src/tools/editor/components/MarkdownEditor.tsx index ec1e1cb9..291e488a 100644 --- a/src/web-ui/src/tools/editor/components/MarkdownEditor.tsx +++ b/src/web-ui/src/tools/editor/components/MarkdownEditor.tsx @@ -284,6 +284,7 @@ const MarkdownEditor: React.FC = ({ placeholder={t('editor.markdownEditor.placeholder')} readonly={readOnly} toolbar={false} + filePath={filePath} basePath={basePath} />
@@ -291,4 +292,3 @@ const MarkdownEditor: React.FC = ({ }; export default MarkdownEditor; - diff --git a/src/web-ui/src/tools/editor/components/PlanViewer.tsx b/src/web-ui/src/tools/editor/components/PlanViewer.tsx index 92da1137..dcf8d79e 100644 --- a/src/web-ui/src/tools/editor/components/PlanViewer.tsx +++ b/src/web-ui/src/tools/editor/components/PlanViewer.tsx @@ -813,6 +813,7 @@ ${JSON.stringify(simpleTodos, null, 2)} placeholder={t('editor.planViewer.contentPlaceholder')} readonly={false} toolbar={false} + filePath={filePath} basePath={basePath} />
diff --git a/src/web-ui/src/tools/editor/core/MonacoEditorCore.tsx b/src/web-ui/src/tools/editor/core/MonacoEditorCore.tsx index 8d7e7938..7fdfc433 100644 --- a/src/web-ui/src/tools/editor/core/MonacoEditorCore.tsx +++ b/src/web-ui/src/tools/editor/core/MonacoEditorCore.tsx @@ -15,6 +15,7 @@ import { monacoModelManager } from '../services/MonacoModelManager'; import { themeManager } from '../services/ThemeManager'; import { editorExtensionManager } from '../services/EditorExtensionManager'; import { buildEditorOptions } from '../services/EditorOptionsBuilder'; +import { activeEditTargetService, createMonacoEditTarget } from '../services/ActiveEditTargetService'; import type { MonacoEditorCoreProps } from './types'; import type { EditorExtensionContext } from '../services/EditorExtensionManager'; import type { EditorOptionsOverrides } from '../services/EditorOptionsBuilder'; @@ -66,6 +67,7 @@ export const MonacoEditorCore = forwardRef(''); const isUnmountedRef = useRef(false); const disposablesRef = useRef([]); + const macosEditorBindingCleanupRef = useRef<(() => void) | null>(null); const hasJumpedRef = useRef(false); const [isReady, setIsReady] = useState(false); @@ -146,6 +148,25 @@ export const MonacoEditorCore = forwardRef { + activeEditTargetService.setActiveTarget(editTarget.id); + }); + const blurDisposable = editor.onDidBlurEditorText(() => { + window.setTimeout(() => { + if (editor.hasTextFocus()) { + return; + } + + activeEditTargetService.clearActiveTarget(editTarget.id); + }, 0); + }); + macosEditorBindingCleanupRef.current = () => { + focusDisposable.dispose(); + blurDisposable.dispose(); + unbindEditTarget(); + }; registerEventListeners(editor, model); @@ -191,6 +212,11 @@ export const MonacoEditorCore = forwardRef d.dispose()); disposablesRef.current = []; + + if (macosEditorBindingCleanupRef.current) { + macosEditorBindingCleanupRef.current(); + macosEditorBindingCleanupRef.current = null; + } if (editorRef.current) { editorRef.current.dispose(); diff --git a/src/web-ui/src/tools/editor/meditor/components/IREditor.scss b/src/web-ui/src/tools/editor/meditor/components/IREditor.scss deleted file mode 100644 index 79c1e239..00000000 --- a/src/web-ui/src/tools/editor/meditor/components/IREditor.scss +++ /dev/null @@ -1,238 +0,0 @@ -@use '../../../../component-library/styles/tokens.scss' as *; - -.m-editor-ir { - width: 100%; - height: 100%; - overflow-y: auto; - padding: $size-gap-4; - - &-placeholder { - color: var(--color-text-disabled); - font-size: $font-size-sm; - padding: $size-gap-2; - cursor: text; - } - - &-block { - margin-bottom: $size-gap-2; - border-radius: $size-radius-base; - transition: all $motion-base $easing-standard; - padding: $size-gap-1; - - &:hover { - background: $element-bg-subtle; - } - - &.editing { - background: transparent; - } - - &.highlighted { - background: rgba(255, 213, 79, 0.15); - animation: highlight-fade 3s ease-out forwards; - } - - @keyframes highlight-fade { - 0% { - background: rgba(255, 213, 79, 0.25); - } - 70% { - background: rgba(255, 213, 79, 0.15); - } - 100% { - background: transparent; - } - } - - &-preview { - padding: $size-gap-2; - cursor: text; - min-height: 24px; - - &.markdown-body { - - > *:first-child { - margin-top: 0; - } - - > *:last-child { - margin-bottom: 0; - } - } - } - - &-textarea { - width: 100%; - padding: $size-gap-2; - border: none !important; - outline: none !important; - box-shadow: none !important; - resize: none; - font-family: $font-family-mono; - font-size: $font-size-sm; - line-height: $line-height-relaxed; - background: transparent; - color: inherit; - overflow: hidden; - min-height: 24px; - box-sizing: border-box; - word-wrap: break-word; - white-space: pre-wrap; - - &:focus { - border: none !important; - outline: none !important; - box-shadow: none !important; - } - - &.m-editor-ir-empty-textarea { - min-height: 100px; - height: 100%; - - &::placeholder { - color: var(--color-text-disabled); - } - } - } - - &-heading { - .m-editor-ir-block-preview { - font-weight: $font-weight-semibold; - } - } - - &-code { - .m-editor-ir-block-preview { - background: transparent; - } - } - - &-blockquote { - .m-editor-ir-block-preview { - border-left: 3px solid rgba(255, 255, 255, 0.15); - padding-left: $size-gap-3; - background: transparent; - border-radius: 0 $size-radius-sm $size-radius-sm 0; - } - } - } - -} - -.m-editor-ir { - .mermaid-container { - margin: $size-gap-2 0; - padding: $size-gap-3; - background: $element-bg-subtle; - border: 1px solid rgba(255, 255, 255, 0.03); - border-radius: $size-radius-base; - overflow-x: auto; - - &.mermaid-rendered { - display: flex; - justify-content: center; - align-items: center; - } - - svg { - max-width: 100%; - height: auto; - } - } - - .mermaid-placeholder { - text-align: center; - color: var(--color-text-muted); - font-size: $font-size-xs; - padding: $size-gap-2; - } - - .mermaid-error { - text-align: left !important; - padding: $size-gap-2 $size-gap-3; - font-size: $font-size-xs; - line-height: $line-height-base; - - &-title { - color: $color-error; - font-weight: $font-weight-medium; - margin-bottom: $size-gap-1; - } - - &-divider { - border: none; - border-top: 1px dashed rgba(255, 255, 255, 0.08); - margin: $size-gap-1 0; - } - - &-detail { - color: $color-text-muted; - white-space: pre-wrap; - word-break: break-word; - } - } -} - -.m-editor-ir { - img { - max-width: 100%; - height: auto; - border-radius: $size-radius-sm; - - &.local-image-loading { - min-width: 100px; - min-height: 60px; - background: $element-bg-subtle; - border: 1px dashed rgba(255, 255, 255, 0.1); - display: flex; - align-items: center; - justify-content: center; - position: relative; - - &::after { - content: var(--m-editor-local-image-loading-text, "Loading image..."); - color: var(--color-text-muted); - font-size: $font-size-xs; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - } - } - - &.local-image-loaded { - animation: image-fade-in $motion-base $easing-standard; - } - - &.local-image-error { - min-width: 100px; - min-height: 60px; - background: rgba(var(--color-error-rgb), 0.1); - border: 1px dashed var(--color-error); - display: flex; - align-items: center; - justify-content: center; - position: relative; - - &::after { - content: var(--m-editor-local-image-failed-text, "Image failed to load"); - color: var(--color-error); - font-size: $font-size-xs; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - } - } - } - - @keyframes image-fade-in { - from { - opacity: 0; - } - to { - opacity: 1; - } - } -} - diff --git a/src/web-ui/src/tools/editor/meditor/components/IREditor.tsx b/src/web-ui/src/tools/editor/meditor/components/IREditor.tsx deleted file mode 100644 index 01868ea6..00000000 --- a/src/web-ui/src/tools/editor/meditor/components/IREditor.tsx +++ /dev/null @@ -1,794 +0,0 @@ -import React, { useState, useCallback, useRef, useEffect, useImperativeHandle, memo, useMemo } from 'react' -import { createLogger } from '@/shared/utils/logger' -import { useI18n } from '@/infrastructure/i18n' -import { useMarkdown } from '../hooks/useMarkdown' -import { useEditorHistory } from '../hooks/useEditorHistory' -import { MermaidService } from '@/tools/mermaid-editor/services/MermaidService' -import { loadLocalImages } from '../utils/loadLocalImages' -import { - isModKey, - toggleBold, - toggleItalic, - insertLink, - indentLines, - outdentLines -} from '../utils/keyboardShortcuts' -import './IREditor.scss' - -const log = createLogger('IREditor') - -function escapeHtml(text: string): string { - return text - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, ''') -} - -interface IREditorProps { - value: string - onChange: (value: string) => void - onFocus?: () => void - onBlur?: () => void - placeholder?: string - readonly?: boolean - autofocus?: boolean - /** - * Directory path of the Markdown file. - * Used to resolve relative image paths. - */ - basePath?: string - /** - * Dirty state change callback. - * Called when dirty state (unsaved changes) changes. - */ - onDirtyChange?: (isDirty: boolean) => void -} - -interface Block { - id: string - type: 'paragraph' | 'code' | 'heading' | 'list' | 'blockquote' | 'other' - startLine: number - endLine: number - content: string -} - -/** IREditor exposed method interface */ -export interface IREditorHandle { - /** Scroll to specified line */ - scrollToLine: (line: number, highlight?: boolean) => void - /** Undo */ - undo: () => boolean - /** Redo */ - redo: () => boolean - /** Whether undo is available */ - canUndo: boolean - /** Whether redo is available */ - canRedo: boolean - /** Focus editor */ - focus: () => void - /** Get current content */ - getContent: () => string - /** Mark current state as saved */ - markSaved: () => void - /** Reset to specified content (used for file loading) */ - setInitialContent: (content: string) => void - /** Whether there are unsaved changes */ - isDirty: boolean -} - -const generateStableBlockId = (startLine: number, type: string) => `block-${startLine}-${type}` - -interface BlockWithoutId { - type: Block['type'] - startLine: number - endLine: number - content: string -} - -const parseMarkdownToBlocksRaw = (content: string): BlockWithoutId[] => { - const lines = content.split('\n') - const newBlocks: BlockWithoutId[] = [] - let currentBlock: BlockWithoutId | null = null - let inCodeBlock = false - let codeBlockStart = -1 - - lines.forEach((line, index) => { - if (line.trim().startsWith('```')) { - if (!inCodeBlock) { - inCodeBlock = true - codeBlockStart = index - } else { - if (currentBlock) { - newBlocks.push(currentBlock) - currentBlock = null - } - newBlocks.push({ - type: 'code', - startLine: codeBlockStart, - endLine: index, - content: lines.slice(codeBlockStart, index + 1).join('\n') - }) - inCodeBlock = false - codeBlockStart = -1 - } - return - } - - if (inCodeBlock) { - return - } - - const isEmptyLine = line.trim() === '' - const prevLineEmpty = index > 0 && lines[index - 1].trim() === '' - - if (isEmptyLine && prevLineEmpty) { - if (currentBlock) { - newBlocks.push(currentBlock) - currentBlock = null - } - return - } - - let blockType: Block['type'] = 'paragraph' - if (!isEmptyLine) { - if (line.match(/^#{1,6}\s/)) { - blockType = 'heading' - } else if (line.match(/^[\*\-\+]\s/) || line.match(/^\d+\.\s/)) { - blockType = 'list' - } else if (line.match(/^>\s/)) { - blockType = 'blockquote' - } - } - - if (currentBlock) { - if (isEmptyLine || currentBlock.type === blockType) { - currentBlock.endLine = index - currentBlock.content += '\n' + line - } else { - newBlocks.push(currentBlock) - currentBlock = { - type: blockType, - startLine: index, - endLine: index, - content: line - } - } - } else if (!isEmptyLine) { - currentBlock = { - type: blockType, - startLine: index, - endLine: index, - content: line - } - } - }) - - if (currentBlock !== null) { - const finalBlock = currentBlock as BlockWithoutId - finalBlock.content = finalBlock.content.replace(/\n+$/, '') - newBlocks.push(finalBlock) - } - - return newBlocks -} - -const updateBlocksSmartly = (oldBlocks: Block[], newRawBlocks: BlockWithoutId[]): Block[] => { - return newRawBlocks.map((rawBlock, index) => { - const oldBlock = oldBlocks[index] - - if (oldBlock && oldBlock.type === rawBlock.type) { - return { - ...rawBlock, - id: oldBlock.id - } - } - - return { - ...rawBlock, - id: generateStableBlockId(rawBlock.startLine, rawBlock.type) - } - }) -} - -export const IREditor = React.forwardRef( - ({ value, onChange, onBlur, placeholder, readonly, basePath, onDirtyChange }, ref) => { - const { t } = useI18n('tools') - const [editingBlockId, setEditingBlockId] = useState(null) - const [editingContent, setEditingContent] = useState('') - const [blocks, setBlocks] = useState([]) - const [highlightedBlockId, setHighlightedBlockId] = useState(null) - const editorRef = useRef(null) - const blockRefsMap = useRef>(new Map()) - const lastValueRef = useRef(value) - - const i18nCssVars = useMemo(() => { - return { - ['--m-editor-local-image-loading-text' as any]: JSON.stringify(t('editor.meditor.localImageLoading')), - ['--m-editor-local-image-failed-text' as any]: JSON.stringify(t('editor.meditor.localImageFailed')), - } as React.CSSProperties - }, [t]) - - const onChangeRef = useRef(onChange) - const onDirtyChangeRef = useRef(onDirtyChange) - onChangeRef.current = onChange - onDirtyChangeRef.current = onDirtyChange - - const history = useEditorHistory({ - initialContent: value, - maxHistorySize: 100, - debounceMs: 300, - onChange: useCallback((newContent: string) => { - lastValueRef.current = newContent - onChangeRef.current(newContent) - }, []), - onDirtyChange: useCallback((isDirty: boolean) => { - onDirtyChangeRef.current?.(isDirty) - }, []) - }) - - const lastExternalValueRef = useRef(value) - useEffect(() => { - if (value !== lastExternalValueRef.current && value !== history.content) { - lastExternalValueRef.current = value - history.setInitialContent(value) - } - }, [value, history]) - - const currentContent = history.content - useEffect(() => { - if (editingBlockId && lastValueRef.current === currentContent) { - return - } - - lastValueRef.current = currentContent - const rawBlocks = parseMarkdownToBlocksRaw(currentContent) - const newBlocks = updateBlocksSmartly(blocks, rawBlocks) - setBlocks(newBlocks) - }, [currentContent, editingBlockId]) // blocks intentionally excluded to avoid loops - - const scrollToLine = useCallback((line: number, highlight: boolean = true) => { - const targetBlock = blocks.find( - block => line >= block.startLine + 1 && line <= block.endLine + 1 - ) - - if (!targetBlock) { - const closestBlock = blocks.reduce((closest, block) => { - const currentDist = Math.min( - Math.abs(block.startLine + 1 - line), - Math.abs(block.endLine + 1 - line) - ) - const closestDist = closest ? Math.min( - Math.abs(closest.startLine + 1 - line), - Math.abs(closest.endLine + 1 - line) - ) : Infinity - return currentDist < closestDist ? block : closest - }, null as Block | null) - - if (closestBlock) { - scrollToBlock(closestBlock.id, highlight) - } - return - } - - scrollToBlock(targetBlock.id, highlight) - }, [blocks]) - - const scrollToBlock = useCallback((blockId: string, highlight: boolean = true) => { - const blockElement = blockRefsMap.current.get(blockId) - - if (blockElement && editorRef.current) { - blockElement.scrollIntoView({ behavior: 'smooth', block: 'center' }) - - if (highlight) { - setHighlightedBlockId(blockId) - setTimeout(() => { - setHighlightedBlockId(prev => prev === blockId ? null : prev) - }, 3000) - } - } - }, []) - - useImperativeHandle(ref, () => ({ - scrollToLine, - undo: history.undo, - redo: history.redo, - canUndo: history.canUndo, - canRedo: history.canRedo, - focus: () => editorRef.current?.focus(), - getContent: () => history.content, - markSaved: history.markSaved, - setInitialContent: history.setInitialContent, - isDirty: history.isDirty - }), [scrollToLine, history]) - - const handleBlockClick = useCallback((blockId: string) => { - if (readonly) return - - const block = blocks.find(b => b.id === blockId) - if (block) { - setEditingBlockId(blockId) - setEditingContent(block.content) - } - }, [readonly, blocks]) - - const handleBlockContentChange = useCallback((newContent: string) => { - setEditingContent(newContent) - - if (editingBlockId && editingBlockId !== 'empty-block') { - const block = blocks.find(b => b.id === editingBlockId) - if (block) { - const lines = currentContent.split('\n') - const before = lines.slice(0, block.startLine).join('\n') - const after = lines.slice(block.endLine + 1).join('\n') - const newValue = [before, newContent, after].filter(s => s !== '').join('\n') - lastValueRef.current = newValue - history.pushChange(newValue) - } - } - }, [editingBlockId, blocks, currentContent, history]) - - const handleBlockBlur = useCallback(() => { - if (!editingBlockId || editingBlockId === 'empty-block') { - setEditingBlockId(null) - setEditingContent('') - onBlur?.() - return - } - - const block = blocks.find(b => b.id === editingBlockId) - if (!block) { - setEditingBlockId(null) - setEditingContent('') - onBlur?.() - return - } - - setEditingBlockId(null) - setEditingContent('') - onBlur?.() - }, [editingBlockId, blocks, onBlur]) - - const handleKeyDown = useCallback((e: React.KeyboardEvent) => { - const modKey = isModKey(e) - - if (e.key === 'Escape') { - e.preventDefault() - setEditingBlockId(null) - setEditingContent('') - const rawBlocks = parseMarkdownToBlocksRaw(currentContent) - setBlocks(updateBlocksSmartly(blocks, rawBlocks)) - return - } - - if (modKey && e.key === 'z' && !e.shiftKey) { - e.preventDefault() - if (editingBlockId) { - setEditingBlockId(null) - setEditingContent('') - } - history.undo() - return - } - - if ((modKey && e.key === 'z' && e.shiftKey) || (e.ctrlKey && e.key === 'y')) { - e.preventDefault() - if (editingBlockId) { - setEditingBlockId(null) - setEditingContent('') - } - history.redo() - return - } - }, [currentContent, blocks, editingBlockId, history]) - - const handleBlockKeyDown = useCallback((e: React.KeyboardEvent) => { - const modKey = isModKey(e) - const textarea = e.currentTarget - const { selectionStart, selectionEnd, value } = textarea - - handleKeyDown(e) - if (e.defaultPrevented) return - - const currentBlockIndex = editingBlockId - ? blocks.findIndex(b => b.id === editingBlockId) - : -1 - const currentBlock = currentBlockIndex >= 0 ? blocks[currentBlockIndex] : null - - if (e.key === 'Backspace' && !modKey && selectionStart === 0 && selectionEnd === 0) { - if (value.trim() === '' && blocks.length > 1) { - e.preventDefault() - - const lines = currentContent.split('\n') - if (currentBlock) { - const before = lines.slice(0, currentBlock.startLine).join('\n') - const after = lines.slice(currentBlock.endLine + 1).join('\n') - const newValue = [before, after].filter(s => s !== '').join('\n\n') - - setEditingBlockId(null) - setEditingContent('') - history.pushChange(newValue) - } - return - } else if (currentBlockIndex > 0 && value.length > 0) { - e.preventDefault() - - const prevBlock = blocks[currentBlockIndex - 1] - const lines = currentContent.split('\n') - const beforePrev = lines.slice(0, prevBlock.startLine).join('\n') - const afterCurrent = lines.slice(currentBlock!.endLine + 1).join('\n') - - const mergedContent = prevBlock.content + '\n' + value - const newValue = [beforePrev, mergedContent, afterCurrent].filter(s => s !== '').join('\n\n') - - setEditingBlockId(null) - setEditingContent('') - history.pushChange(newValue) - - setTimeout(() => { - const newBlocks = parseMarkdownToBlocksRaw(newValue) - if (newBlocks.length > 0 && currentBlockIndex - 1 < newBlocks.length) { - const targetBlock = newBlocks[currentBlockIndex - 1] - const newBlockId = generateStableBlockId(targetBlock.startLine, targetBlock.type) - setEditingBlockId(newBlockId) - setEditingContent(targetBlock.content) - } - }, 50) - return - } - } - - if (modKey && e.key === 'Enter') { - e.preventDefault() - - if (currentBlock) { - const lines = currentContent.split('\n') - const before = lines.slice(0, currentBlock.endLine + 1).join('\n') - const after = lines.slice(currentBlock.endLine + 1).join('\n') - const newValue = before + '\n\n' + after - - setEditingBlockId(null) - setEditingContent('') - history.pushChange(newValue) - } - return - } - - if (e.key === 'Tab' && !modKey) { - e.preventDefault() - const result = e.shiftKey - ? outdentLines(value, selectionStart, selectionEnd) - : indentLines(value, selectionStart, selectionEnd) - - textarea.value = result.text - textarea.setSelectionRange(result.selectionStart, result.selectionEnd) - const event = new Event('input', { bubbles: true }) - textarea.dispatchEvent(event) - return - } - - if (modKey && e.key === 'b') { - e.preventDefault() - const result = toggleBold(value, selectionStart, selectionEnd) - textarea.value = result.text - textarea.setSelectionRange(result.selectionStart, result.selectionEnd) - const event = new Event('input', { bubbles: true }) - textarea.dispatchEvent(event) - return - } - - if (modKey && e.key === 'i') { - e.preventDefault() - const result = toggleItalic(value, selectionStart, selectionEnd) - textarea.value = result.text - textarea.setSelectionRange(result.selectionStart, result.selectionEnd) - const event = new Event('input', { bubbles: true }) - textarea.dispatchEvent(event) - return - } - - if (modKey && e.key === 'k') { - e.preventDefault() - const result = insertLink(value, selectionStart, selectionEnd) - textarea.value = result.text - textarea.setSelectionRange(result.selectionStart, result.selectionEnd) - const event = new Event('input', { bubbles: true }) - textarea.dispatchEvent(event) - return - } - }, [handleKeyDown, editingBlockId, blocks, currentContent, history]) - - const handleEmptyBlockChange = useCallback((e: React.ChangeEvent) => { - history.pushChange(e.target.value) - }, [history]) - - const handleEmptyBlockKeyDown = useCallback((e: React.KeyboardEvent) => { - const modKey = isModKey(e) - const textarea = e.currentTarget - const { selectionStart, selectionEnd, value } = textarea - - handleKeyDown(e) - if (e.defaultPrevented) return - - if (e.key === 'Tab' && !modKey) { - e.preventDefault() - const result = e.shiftKey - ? outdentLines(value, selectionStart, selectionEnd) - : indentLines(value, selectionStart, selectionEnd) - - history.pushChange(result.text) - setTimeout(() => { - textarea.setSelectionRange(result.selectionStart, result.selectionEnd) - }, 0) - return - } - - if (modKey && e.key === 'b') { - e.preventDefault() - const result = toggleBold(value, selectionStart, selectionEnd) - history.pushChange(result.text) - setTimeout(() => { - textarea.setSelectionRange(result.selectionStart, result.selectionEnd) - }, 0) - return - } - - if (modKey && e.key === 'i') { - e.preventDefault() - const result = toggleItalic(value, selectionStart, selectionEnd) - history.pushChange(result.text) - setTimeout(() => { - textarea.setSelectionRange(result.selectionStart, result.selectionEnd) - }, 0) - return - } - - if (modKey && e.key === 'k') { - e.preventDefault() - const result = insertLink(value, selectionStart, selectionEnd) - history.pushChange(result.text) - setTimeout(() => { - textarea.setSelectionRange(result.selectionStart, result.selectionEnd) - }, 0) - return - } - }, [handleKeyDown, history]) - - const handleEmptyBlockClick = useCallback(() => { - if (!readonly) { - setEditingBlockId('empty-block') - } - }, [readonly]) - - const handleEmptyBlockBlur = useCallback(() => { - setEditingBlockId(null) - onBlur?.() - }, [onBlur]) - - return ( -
- {editingBlockId === 'empty-block' ? ( -
-