mirror of
https://github.com/davidkaya/aryx.git
synced 2026-07-23 21:18:40 +02:00
feat: add markdown composer backend foundation
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -6,8 +6,15 @@
|
||||
"name": "kopaya",
|
||||
"dependencies": {
|
||||
"@dagrejs/dagre": "^3.0.0",
|
||||
"@lexical/code": "0.42.0",
|
||||
"@lexical/link": "0.42.0",
|
||||
"@lexical/list": "0.42.0",
|
||||
"@lexical/markdown": "0.42.0",
|
||||
"@lexical/react": "0.42.0",
|
||||
"@lexical/rich-text": "0.42.0",
|
||||
"@xyflow/react": "^12.10.1",
|
||||
"keytar": "^7.9.0",
|
||||
"lexical": "0.42.0",
|
||||
"lucide-react": "^0.577.0",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
@@ -15,6 +22,7 @@
|
||||
"remark-gfm": "^4.0.1",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lexical/headless": "0.42.0",
|
||||
"@tailwindcss/vite": "^4.2.2",
|
||||
"@types/node": "^25.5.0",
|
||||
"@types/react": "^19.2.14",
|
||||
@@ -130,6 +138,16 @@
|
||||
|
||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
|
||||
|
||||
"@floating-ui/core": ["@floating-ui/core@1.7.5", "", { "dependencies": { "@floating-ui/utils": "^0.2.11" } }, "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ=="],
|
||||
|
||||
"@floating-ui/dom": ["@floating-ui/dom@1.7.6", "", { "dependencies": { "@floating-ui/core": "^1.7.5", "@floating-ui/utils": "^0.2.11" } }, "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ=="],
|
||||
|
||||
"@floating-ui/react": ["@floating-ui/react@0.27.19", "", { "dependencies": { "@floating-ui/react-dom": "^2.1.8", "@floating-ui/utils": "^0.2.11", "tabbable": "^6.0.0" }, "peerDependencies": { "react": ">=17.0.0", "react-dom": ">=17.0.0" } }, "sha512-31B8h5mm8YxotlE7/AU/PhNAl8eWxAmjL/v2QOxroDNkTFLk3Uu82u63N3b6TXa4EGJeeZLVcd/9AlNlVqzeog=="],
|
||||
|
||||
"@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.8", "", { "dependencies": { "@floating-ui/dom": "^1.7.6" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A=="],
|
||||
|
||||
"@floating-ui/utils": ["@floating-ui/utils@0.2.11", "", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="],
|
||||
|
||||
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
||||
|
||||
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
|
||||
@@ -140,8 +158,60 @@
|
||||
|
||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||
|
||||
"@lexical/clipboard": ["@lexical/clipboard@0.42.0", "", { "dependencies": { "@lexical/html": "0.42.0", "@lexical/list": "0.42.0", "@lexical/selection": "0.42.0", "@lexical/utils": "0.42.0", "lexical": "0.42.0" } }, "sha512-D3K2ID0zew/+CKpwxnUTTh/N46yU4IK8bFWV9Htz+g1vFhgUF9UnDOQCmqpJbdP7z+9U1F8rk3fzf9OmP2Fm2w=="],
|
||||
|
||||
"@lexical/code": ["@lexical/code@0.42.0", "", { "dependencies": { "@lexical/code-core": "0.42.0", "@lexical/code-prism": "0.42.0", "lexical": "0.42.0" } }, "sha512-KMu1nWae9pHvA9nl6dlJacbt3QBBNemgalmLJcZ5QhdGEQA1cVIU4gBPJ5TJqgY9XF7WZgj5JvDIPxjrZmf+XQ=="],
|
||||
|
||||
"@lexical/code-core": ["@lexical/code-core@0.42.0", "", { "dependencies": { "lexical": "0.42.0" } }, "sha512-vrZTUPWDJkHjAAvuV2+Qte4vYE80s7hIO7wxipiJmWojGx6lcmQjO+UqJ8AIrqI4Wjy8kXrK74kisApWmwxuCw=="],
|
||||
|
||||
"@lexical/code-prism": ["@lexical/code-prism@0.42.0", "", { "dependencies": { "@lexical/code-core": "0.42.0", "lexical": "0.42.0", "prismjs": "^1.30.0" } }, "sha512-KgngkUtgcgC8ocBnfGyN71CC3EnP5PMFAmH1KcGp/+jSgl11nRpCjwYYIoUHm6AB7jKJ8dLbd/UUmShARjUnGA=="],
|
||||
|
||||
"@lexical/devtools-core": ["@lexical/devtools-core@0.42.0", "", { "dependencies": { "@lexical/html": "0.42.0", "@lexical/link": "0.42.0", "@lexical/mark": "0.42.0", "@lexical/table": "0.42.0", "@lexical/utils": "0.42.0", "lexical": "0.42.0" }, "peerDependencies": { "react": ">=17.x", "react-dom": ">=17.x" } }, "sha512-8nP8eE9i8JImgSrvInkWFfMCmXVKp3w3VaOvbJysdlK/Zal6xd8EWJEi6elj0mUW5T/oycfipPs2Sfl7Z+n14A=="],
|
||||
|
||||
"@lexical/dragon": ["@lexical/dragon@0.42.0", "", { "dependencies": { "@lexical/extension": "0.42.0", "lexical": "0.42.0" } }, "sha512-/TQzP+7PLJMqq9+MlgQWiJsxS9GOOa8Gp0svCD8vNIOciYmXfd28TR1Go+ZnBWwr7k/2W++3XUYVQU2KUcQsDQ=="],
|
||||
|
||||
"@lexical/extension": ["@lexical/extension@0.42.0", "", { "dependencies": { "@lexical/utils": "0.42.0", "@preact/signals-core": "^1.11.0", "lexical": "0.42.0" } }, "sha512-rkZq/h8d1BenKRqU4t/zQUVfY/RinMX1Tz7t+Ee3ss0sk+kzP4W+URXNAxpn7r39Vn6wrFBqmCziah3dLAIqPw=="],
|
||||
|
||||
"@lexical/hashtag": ["@lexical/hashtag@0.42.0", "", { "dependencies": { "@lexical/text": "0.42.0", "@lexical/utils": "0.42.0", "lexical": "0.42.0" } }, "sha512-WOg5nFOfhabNBXzEIutdWDj+TUHtJEezj6w8jyYDGqZ31gu0cgrXSeV8UIynz/1oj+rpzEeEB7P6ODnwgjt7qA=="],
|
||||
|
||||
"@lexical/headless": ["@lexical/headless@0.42.0", "", { "dependencies": { "happy-dom": "^20.0.0", "lexical": "0.42.0" } }, "sha512-/bfeYhRn82IWfZZkn9XW37cfEOVjKZyjVrKfXNgI14gBvdMuX9aN5CSmr4yEusIJJ4svLzKV0STuMWEopn6Iag=="],
|
||||
|
||||
"@lexical/history": ["@lexical/history@0.42.0", "", { "dependencies": { "@lexical/extension": "0.42.0", "@lexical/utils": "0.42.0", "lexical": "0.42.0" } }, "sha512-YfCZ1ICUt6BCg2ncJWFMuS4yftnB7FEHFRf3qqTSTf6oGZ4IZfzabMNEy47xybUuf7FXBbdaCKJrc/zOM+wGxw=="],
|
||||
|
||||
"@lexical/html": ["@lexical/html@0.42.0", "", { "dependencies": { "@lexical/selection": "0.42.0", "@lexical/utils": "0.42.0", "lexical": "0.42.0" } }, "sha512-KgBUDLXehufCsXW3w0XsuoI2xecIhouOishnaNOH4zIA7dAtnNAfdPN/kWrWs0s83gz44OrnqccP+Bprw3UDEQ=="],
|
||||
|
||||
"@lexical/link": ["@lexical/link@0.42.0", "", { "dependencies": { "@lexical/extension": "0.42.0", "@lexical/utils": "0.42.0", "lexical": "0.42.0" } }, "sha512-cdeM/+f+kn7aGwW/3FIi6USjl1gBNdEEwg0/ZS+KlYcsy8gxx2e4cyVjsomBu/WU17Qxa0NC0paSr7qEJ/1Fig=="],
|
||||
|
||||
"@lexical/list": ["@lexical/list@0.42.0", "", { "dependencies": { "@lexical/extension": "0.42.0", "@lexical/selection": "0.42.0", "@lexical/utils": "0.42.0", "lexical": "0.42.0" } }, "sha512-TIezILnmIVuvfqEEbcMnsT4xQRlswI6ysHISqsvKL6l5EBhs1gqmNYjHa/Yrfzaq5y52TM1PAtxbFts+G7N6kg=="],
|
||||
|
||||
"@lexical/mark": ["@lexical/mark@0.42.0", "", { "dependencies": { "@lexical/utils": "0.42.0", "lexical": "0.42.0" } }, "sha512-H1aGjbMEcL4B8GT7bm/ePHm7j3Wema+wIRNPmxMtXGMz5gpVN3gZlvg2UcUHHJb00SrBA95OUVT5I2nu/KP06w=="],
|
||||
|
||||
"@lexical/markdown": ["@lexical/markdown@0.42.0", "", { "dependencies": { "@lexical/code-core": "0.42.0", "@lexical/link": "0.42.0", "@lexical/list": "0.42.0", "@lexical/rich-text": "0.42.0", "@lexical/text": "0.42.0", "@lexical/utils": "0.42.0", "lexical": "0.42.0" } }, "sha512-+mOxgBiumlgVX8Acna+9HjJfSOw1jywufGcAQq3/8S11wZ4gE0u13AaR8LMmU8ydVeOQg09y8PNzGNQ/avZJbg=="],
|
||||
|
||||
"@lexical/offset": ["@lexical/offset@0.42.0", "", { "dependencies": { "lexical": "0.42.0" } }, "sha512-V+4af1KmTOnBZrR+kU3e6eD33W/g3QqMPPp3cpFwyXk/dKRc4K8HfyDsSDrjop1mPd9pl3lKSiEmX6uQG8K9XQ=="],
|
||||
|
||||
"@lexical/overflow": ["@lexical/overflow@0.42.0", "", { "dependencies": { "lexical": "0.42.0" } }, "sha512-wlrHaM27rODJP5m+CTgfZGLg3qWlQ0ptGodcqoGdq6HSbV8nGFY6TvcLMaMtYQ1lm4v9G7Xe9LwjooR6xS3Gug=="],
|
||||
|
||||
"@lexical/plain-text": ["@lexical/plain-text@0.42.0", "", { "dependencies": { "@lexical/clipboard": "0.42.0", "@lexical/dragon": "0.42.0", "@lexical/selection": "0.42.0", "@lexical/utils": "0.42.0", "lexical": "0.42.0" } }, "sha512-YWvBwIxLltrIaZDcv0rK4s44P6Yt17yhOb0E+g3+tjF8GGPrrocox+Pglu0m2RHR+G7zULN3isolmWIm/HhWiw=="],
|
||||
|
||||
"@lexical/react": ["@lexical/react@0.42.0", "", { "dependencies": { "@floating-ui/react": "^0.27.16", "@lexical/devtools-core": "0.42.0", "@lexical/dragon": "0.42.0", "@lexical/extension": "0.42.0", "@lexical/hashtag": "0.42.0", "@lexical/history": "0.42.0", "@lexical/link": "0.42.0", "@lexical/list": "0.42.0", "@lexical/mark": "0.42.0", "@lexical/markdown": "0.42.0", "@lexical/overflow": "0.42.0", "@lexical/plain-text": "0.42.0", "@lexical/rich-text": "0.42.0", "@lexical/table": "0.42.0", "@lexical/text": "0.42.0", "@lexical/utils": "0.42.0", "@lexical/yjs": "0.42.0", "lexical": "0.42.0", "react-error-boundary": "^6.0.0" }, "peerDependencies": { "react": ">=17.x", "react-dom": ">=17.x" } }, "sha512-ujWJXhvlFVVTpwDcnSgEYWRuqUbreZaMB+4bjIDT5r7hkAplUHQndlkeuFHKFiJBasSAreleV7zhXrLL5xa9eA=="],
|
||||
|
||||
"@lexical/rich-text": ["@lexical/rich-text@0.42.0", "", { "dependencies": { "@lexical/clipboard": "0.42.0", "@lexical/dragon": "0.42.0", "@lexical/selection": "0.42.0", "@lexical/utils": "0.42.0", "lexical": "0.42.0" } }, "sha512-v4YgiM3oK3FZcRrfB+LetvLbQ5aee9MRO9tHf0EFweXg19XnSjHV0cfPAW7TyPxRELzB69+K0Q3AybRlTMjG4Q=="],
|
||||
|
||||
"@lexical/selection": ["@lexical/selection@0.42.0", "", { "dependencies": { "lexical": "0.42.0" } }, "sha512-iWTjLA5BSEuUnvWe9Xwu9FSdZFl3Yi0NqalabXKI+7KgCIlIVXE74y4NvWPUSLkSCB/Z1RPKiHmZqZ1vyu/yGQ=="],
|
||||
|
||||
"@lexical/table": ["@lexical/table@0.42.0", "", { "dependencies": { "@lexical/clipboard": "0.42.0", "@lexical/extension": "0.42.0", "@lexical/utils": "0.42.0", "lexical": "0.42.0" } }, "sha512-GKiZyjQsHDXRckq5VBrOowyvds51WoVRECfDgcl8pqLMnKyEdCa58E7fkSJrr5LS80Scod+Cjn6SBRzOcdsrKg=="],
|
||||
|
||||
"@lexical/text": ["@lexical/text@0.42.0", "", { "dependencies": { "lexical": "0.42.0" } }, "sha512-hT3EYVtBmONXyXe4TFVgtFcG1tf6JhLEuAf95+cOjgFGFSgvkZ/64BPbKLNTj2/9n6cU7EGPUNNwVigCSECJ2g=="],
|
||||
|
||||
"@lexical/utils": ["@lexical/utils@0.42.0", "", { "dependencies": { "@lexical/selection": "0.42.0", "lexical": "0.42.0" } }, "sha512-wGNdCW3QWEyVdFiSTLZfFPtiASPyYLcekIiYYZmoRVxVimT/jY+QPfnkO4JYgkO7Z70g/dsg9OhqyQSChQfvkQ=="],
|
||||
|
||||
"@lexical/yjs": ["@lexical/yjs@0.42.0", "", { "dependencies": { "@lexical/offset": "0.42.0", "@lexical/selection": "0.42.0", "lexical": "0.42.0" }, "peerDependencies": { "yjs": ">=13.5.22" } }, "sha512-DplzWnYhfFceGPR+UyDFpZdB287wF/vNOHFuDsBF/nGDdTezvr0Gf60opzyBEF3oXym6p3xTmGygxvO97LZ+vw=="],
|
||||
|
||||
"@malept/cross-spawn-promise": ["@malept/cross-spawn-promise@1.1.1", "", { "dependencies": { "cross-spawn": "^7.0.1" } }, "sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ=="],
|
||||
|
||||
"@preact/signals-core": ["@preact/signals-core@1.14.0", "", {}, "sha512-AowtCcCU/33lFlh1zRFf/u+12rfrhtNakj7UpaGEsmMwUKpKWMVvcktOGcwBBNiB4lWrZWc01LhiyyzVklJyaQ=="],
|
||||
|
||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.43", "", {}, "sha512-5Uxg7fQUCmfhax7FJke2+8B6cqgeUJUD9o2uXIKXhD+mG0mL6NObmVoi9wXEU1tY89mZKgAYA6fTbftx3q2ZPQ=="],
|
||||
|
||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.0", "", { "os": "android", "cpu": "arm" }, "sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A=="],
|
||||
@@ -276,6 +346,10 @@
|
||||
|
||||
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
|
||||
|
||||
"@types/whatwg-mimetype": ["@types/whatwg-mimetype@3.0.2", "", {}, "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA=="],
|
||||
|
||||
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
|
||||
|
||||
"@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="],
|
||||
|
||||
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
|
||||
@@ -388,6 +462,8 @@
|
||||
|
||||
"enhanced-resolve": ["enhanced-resolve@5.20.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA=="],
|
||||
|
||||
"entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
|
||||
|
||||
"env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
|
||||
|
||||
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
||||
@@ -436,6 +512,8 @@
|
||||
|
||||
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||
|
||||
"happy-dom": ["happy-dom@20.8.8", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "entities": "^7.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.18.3" } }, "sha512-5/F8wxkNxYtsN0bXfMwIyNLZ9WYsoOYPbmoluqVJqv8KBUbcyKZawJ7uYK4WTX8IHBLYv+VXIwfeNDPy1oKMwQ=="],
|
||||
|
||||
"has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="],
|
||||
|
||||
"hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="],
|
||||
@@ -472,6 +550,8 @@
|
||||
|
||||
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
|
||||
"isomorphic.js": ["isomorphic.js@0.2.5", "", {}, "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw=="],
|
||||
|
||||
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
|
||||
|
||||
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||
@@ -490,6 +570,10 @@
|
||||
|
||||
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
|
||||
|
||||
"lexical": ["lexical@0.42.0", "", {}, "sha512-GY9Lg3YEIU7nSFaiUlLspZ1fm4NfIcfABaxy9nT+fRVDkX7iV005T5Swil83gXUmxFUNKGal3j+hUxHOUDr+Aw=="],
|
||||
|
||||
"lib0": ["lib0@0.2.117", "", { "dependencies": { "isomorphic.js": "^0.2.4" }, "bin": { "0serve": "bin/0serve.js", "0gentesthtml": "bin/gentesthtml.js", "0ecdsa-generate-keypair": "bin/0ecdsa-generate-keypair.js" } }, "sha512-DeXj9X5xDCjgKLU/7RR+/HQEVzuuEUiwldwOGsHK/sfAfELGWEyTcf0x+uOvCvK3O2zPmZePXWL85vtia6GyZw=="],
|
||||
|
||||
"lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
|
||||
|
||||
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
|
||||
@@ -654,6 +738,8 @@
|
||||
|
||||
"prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="],
|
||||
|
||||
"prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="],
|
||||
|
||||
"progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="],
|
||||
|
||||
"property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="],
|
||||
@@ -670,6 +756,8 @@
|
||||
|
||||
"react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="],
|
||||
|
||||
"react-error-boundary": ["react-error-boundary@6.1.1", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-BrYwPOdXi5mqkk5lw+Uvt0ThHx32rCt3BkukS4X23A2AIWDPSGX6iaWTc0y9TU/mHDA/6qOSGel+B2ERkOvD1w=="],
|
||||
|
||||
"react-markdown": ["react-markdown@10.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ=="],
|
||||
|
||||
"react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="],
|
||||
@@ -728,6 +816,8 @@
|
||||
|
||||
"sumchecker": ["sumchecker@3.0.1", "", { "dependencies": { "debug": "^4.1.0" } }, "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg=="],
|
||||
|
||||
"tabbable": ["tabbable@6.4.0", "", {}, "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg=="],
|
||||
|
||||
"tailwindcss": ["tailwindcss@4.2.2", "", {}, "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q=="],
|
||||
|
||||
"tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
|
||||
@@ -778,14 +868,20 @@
|
||||
|
||||
"vite": ["vite@7.1.10", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-CmuvUBzVJ/e3HGxhg6cYk88NGgTnBoOo7ogtfJJ0fefUWAxN/WDSUa50o+oVBxuIhO8FoEZW0j2eW7sfjs5EtA=="],
|
||||
|
||||
"whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="],
|
||||
|
||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
||||
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
||||
|
||||
"ws": ["ws@8.20.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA=="],
|
||||
|
||||
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
||||
|
||||
"yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="],
|
||||
|
||||
"yjs": ["yjs@13.6.30", "", { "dependencies": { "lib0": "^0.2.99" } }, "sha512-vv/9h42eCMC81ZHDFswuu/MKzkl/vyq1BhaNGfHyOonwlG4CJbQF4oiBBJPvfdeCt/PlVDWh7Nov9D34YY09uQ=="],
|
||||
|
||||
"zustand": ["zustand@4.5.7", "", { "dependencies": { "use-sync-external-store": "^1.2.2" }, "peerDependencies": { "@types/react": ">=16.8", "immer": ">=9.0.6", "react": ">=16.8" }, "optionalPeers": ["@types/react", "immer", "react"] }, "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw=="],
|
||||
|
||||
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
"homepage": "https://github.com/davidkaya/eryx#readme",
|
||||
"packageManager": "bun@1.3.6",
|
||||
"devDependencies": {
|
||||
"@lexical/headless": "0.42.0",
|
||||
"@tailwindcss/vite": "^4.2.2",
|
||||
"@types/node": "^25.5.0",
|
||||
"@types/react": "^19.2.14",
|
||||
@@ -47,8 +48,15 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@dagrejs/dagre": "^3.0.0",
|
||||
"@lexical/code": "0.42.0",
|
||||
"@lexical/link": "0.42.0",
|
||||
"@lexical/list": "0.42.0",
|
||||
"@lexical/markdown": "0.42.0",
|
||||
"@lexical/react": "0.42.0",
|
||||
"@lexical/rich-text": "0.42.0",
|
||||
"@xyflow/react": "^12.10.1",
|
||||
"keytar": "^7.9.0",
|
||||
"lexical": "0.42.0",
|
||||
"lucide-react": "^0.577.0",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
|
||||
@@ -57,6 +57,7 @@ import {
|
||||
type ChatMessageRecord,
|
||||
type SessionRecord,
|
||||
} from '@shared/domain/session';
|
||||
import { prepareChatMessageContent } from '@shared/utils/chatMessage';
|
||||
import {
|
||||
appendRunActivityEvent,
|
||||
completeSessionRunRecord,
|
||||
@@ -442,8 +443,8 @@ export class EryxAppService extends EventEmitter<AppServiceEvents> {
|
||||
const pattern = this.requirePattern(workspace, session.patternId);
|
||||
const effectivePattern = await this.buildEffectivePattern(project, pattern, session);
|
||||
|
||||
const trimmed = content.trim();
|
||||
if (!trimmed) {
|
||||
const preparedContent = prepareChatMessageContent(content);
|
||||
if (!preparedContent) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -455,7 +456,7 @@ export class EryxAppService extends EventEmitter<AppServiceEvents> {
|
||||
id: userMessageId,
|
||||
role: 'user',
|
||||
authorName: 'You',
|
||||
content: trimmed,
|
||||
content: preparedContent,
|
||||
createdAt: occurredAt,
|
||||
});
|
||||
session.title = resolveSessionTitle(session, effectivePattern, session.messages);
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
import { reasoningEffortOptions, type PatternDefinition, type ReasoningEffort } from '@shared/domain/pattern';
|
||||
import { isScratchpadProject, type ProjectRecord } from '@shared/domain/project';
|
||||
import { resolveSessionToolingSelection, type SessionRecord } from '@shared/domain/session';
|
||||
import { hasMeaningfulChatMessageContent, prepareChatMessageContent } from '@shared/utils/chatMessage';
|
||||
import {
|
||||
listApprovalToolDefinitions,
|
||||
type ApprovalToolDefinition,
|
||||
@@ -694,6 +695,7 @@ export function ChatPane({
|
||||
const supportedEfforts = getSupportedReasoningEfforts(selectedModel);
|
||||
const scratchpadReasoningEffort = resolveReasoningEffort(selectedModel, primaryAgent?.reasoningEffort);
|
||||
const isComposerDisabled = isSessionBusy || isUpdatingScratchpadConfig;
|
||||
const canSubmitInput = hasMeaningfulChatMessageContent(input) && !isComposerDisabled;
|
||||
|
||||
const toolSelection = useMemo(() => resolveSessionToolingSelection(session), [session]);
|
||||
const mcpServers = toolingSettings.mcpServers;
|
||||
@@ -729,7 +731,7 @@ export function ChatPane({
|
||||
}, [session.id]);
|
||||
|
||||
async function handleSubmit() {
|
||||
const text = input.trim();
|
||||
const text = prepareChatMessageContent(input);
|
||||
if (!text || isComposerDisabled) return;
|
||||
setInput('');
|
||||
await onSend(text);
|
||||
@@ -1042,11 +1044,11 @@ export function ChatPane({
|
||||
/>
|
||||
<button
|
||||
className={`absolute bottom-2 right-2 flex size-8 items-center justify-center rounded-lg transition ${
|
||||
input.trim() && !isComposerDisabled
|
||||
canSubmitInput
|
||||
? 'bg-indigo-600 text-white hover:bg-indigo-500'
|
||||
: 'bg-zinc-800 text-zinc-600'
|
||||
}`}
|
||||
disabled={isComposerDisabled || !input.trim()}
|
||||
disabled={!canSubmitInput}
|
||||
onClick={() => void handleSubmit()}
|
||||
type="button"
|
||||
>
|
||||
|
||||
@@ -1,113 +1,5 @@
|
||||
import { useState, type ReactNode } from 'react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import { Check, Clipboard } from 'lucide-react';
|
||||
|
||||
function extractText(node: unknown): string {
|
||||
if (!node || typeof node !== 'object') return '';
|
||||
const n = node as Record<string, unknown>;
|
||||
if (n.type === 'text') return String(n.value ?? '');
|
||||
if (Array.isArray(n.children)) {
|
||||
return (n.children as unknown[]).map(extractText).join('');
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function CodeBlock({ language, children }: { language: string; children: string }) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
function handleCopy() {
|
||||
void navigator.clipboard.writeText(children);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="group relative my-3 overflow-hidden rounded-lg border border-zinc-800 bg-[#0d0d10]">
|
||||
<div className="flex items-center justify-between border-b border-zinc-800/80 px-4 py-1.5">
|
||||
<span className="select-none text-[11px] text-zinc-500">
|
||||
{language || 'text'}
|
||||
</span>
|
||||
<button
|
||||
className="flex items-center gap-1 text-[11px] text-zinc-500 transition hover:text-zinc-300"
|
||||
onClick={handleCopy}
|
||||
type="button"
|
||||
>
|
||||
{copied ? (
|
||||
<>
|
||||
<Check className="size-3" />
|
||||
Copied!
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Clipboard className="size-3" />
|
||||
Copy
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<pre className="overflow-x-auto p-4">
|
||||
<code className="font-mono text-[13px] leading-relaxed text-zinc-300">
|
||||
{children}
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const markdownComponents: Record<string, (props: Record<string, unknown>) => ReactNode> = {
|
||||
pre({ children, node }: Record<string, unknown>) {
|
||||
const codeChild = (node as Record<string, unknown[]>)?.children?.[0] as
|
||||
| Record<string, unknown>
|
||||
| undefined;
|
||||
|
||||
if (codeChild && (codeChild as Record<string, string>).tagName === 'code') {
|
||||
const classNames = ((codeChild.properties as Record<string, string[]>)?.className ??
|
||||
[]) as string[];
|
||||
const hasLanguage = classNames.some((c) => c.startsWith('language-'));
|
||||
|
||||
if (!hasLanguage) {
|
||||
const text = extractText(codeChild).replace(/\n$/, '');
|
||||
return <CodeBlock language="">{text}</CodeBlock>;
|
||||
}
|
||||
}
|
||||
|
||||
return <>{children as ReactNode}</>;
|
||||
},
|
||||
|
||||
code({ className, children, ...rest }: Record<string, unknown>) {
|
||||
const match = /language-(\w+)/.exec(String(className ?? ''));
|
||||
if (match) {
|
||||
return (
|
||||
<CodeBlock language={match[1]}>
|
||||
{String(children).replace(/\n$/, '')}
|
||||
</CodeBlock>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<code
|
||||
className="rounded bg-zinc-800 px-1.5 py-0.5 font-mono text-[0.875em] text-zinc-200"
|
||||
{...rest}
|
||||
>
|
||||
{children as ReactNode}
|
||||
</code>
|
||||
);
|
||||
},
|
||||
|
||||
a({ href, children }: Record<string, unknown>) {
|
||||
return (
|
||||
<a
|
||||
className="text-indigo-400 underline underline-offset-2 transition hover:text-indigo-300"
|
||||
href={String(href)}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{children as ReactNode}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
};
|
||||
import { chatMarkdownComponents, chatMarkdownRemarkPlugins } from '@renderer/lib/chatMarkdown';
|
||||
|
||||
interface MarkdownContentProps {
|
||||
content: string;
|
||||
@@ -117,8 +9,8 @@ export function MarkdownContent({ content }: MarkdownContentProps) {
|
||||
return (
|
||||
<div className="markdown-content">
|
||||
<ReactMarkdown
|
||||
components={markdownComponents}
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={chatMarkdownComponents}
|
||||
remarkPlugins={chatMarkdownRemarkPlugins}
|
||||
>
|
||||
{content}
|
||||
</ReactMarkdown>
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
import { useState, type ReactNode } from 'react';
|
||||
import type { Components } from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import { Check, Clipboard } from 'lucide-react';
|
||||
|
||||
function extractText(node: unknown): string {
|
||||
if (!node || typeof node !== 'object') return '';
|
||||
const n = node as Record<string, unknown>;
|
||||
if (n.type === 'text') return String(n.value ?? '');
|
||||
if (Array.isArray(n.children)) {
|
||||
return (n.children as unknown[]).map(extractText).join('');
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function CodeBlock({ language, children }: { language: string; children: string }) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
function handleCopy() {
|
||||
void navigator.clipboard.writeText(children);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="group relative my-3 overflow-hidden rounded-lg border border-zinc-800 bg-[#0d0d10]">
|
||||
<div className="flex items-center justify-between border-b border-zinc-800/80 px-4 py-1.5">
|
||||
<span className="select-none text-[11px] text-zinc-500">
|
||||
{language || 'text'}
|
||||
</span>
|
||||
<button
|
||||
className="flex items-center gap-1 text-[11px] text-zinc-500 transition hover:text-zinc-300"
|
||||
onClick={handleCopy}
|
||||
type="button"
|
||||
>
|
||||
{copied ? (
|
||||
<>
|
||||
<Check className="size-3" />
|
||||
Copied!
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Clipboard className="size-3" />
|
||||
Copy
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<pre className="overflow-x-auto p-4">
|
||||
<code className="font-mono text-[13px] leading-relaxed text-zinc-300">
|
||||
{children}
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const chatMarkdownRemarkPlugins = [remarkGfm];
|
||||
|
||||
export const chatMarkdownComponents: Components = {
|
||||
pre({ children, node }) {
|
||||
const nodeChildren =
|
||||
typeof node === 'object' && node !== null && Array.isArray((node as { children?: unknown[] }).children)
|
||||
? (node as { children: unknown[] }).children
|
||||
: undefined;
|
||||
const codeChild = nodeChildren?.[0] as Record<string, unknown> | undefined;
|
||||
|
||||
if (codeChild && (codeChild as Record<string, string>).tagName === 'code') {
|
||||
const classNames = ((codeChild.properties as Record<string, string[]>)?.className ??
|
||||
[]) as string[];
|
||||
const hasLanguage = classNames.some((className) => className.startsWith('language-'));
|
||||
|
||||
if (!hasLanguage) {
|
||||
const text = extractText(codeChild).replace(/\n$/, '');
|
||||
return <CodeBlock language="">{text}</CodeBlock>;
|
||||
}
|
||||
}
|
||||
|
||||
return <>{children as ReactNode}</>;
|
||||
},
|
||||
|
||||
code({ className, children, ...rest }) {
|
||||
const match = /language-(\w+)/.exec(String(className ?? ''));
|
||||
if (match) {
|
||||
return (
|
||||
<CodeBlock language={match[1]}>
|
||||
{String(children).replace(/\n$/, '')}
|
||||
</CodeBlock>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<code
|
||||
className="rounded bg-zinc-800 px-1.5 py-0.5 font-mono text-[0.875em] text-zinc-200"
|
||||
{...rest}
|
||||
>
|
||||
{children as ReactNode}
|
||||
</code>
|
||||
);
|
||||
},
|
||||
|
||||
a({ href, children }) {
|
||||
return (
|
||||
<a
|
||||
className="text-indigo-400 underline underline-offset-2 transition hover:text-indigo-300"
|
||||
href={String(href)}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{children as ReactNode}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,97 @@
|
||||
import { CodeNode } from '@lexical/code';
|
||||
import { AutoLinkNode, LinkNode } from '@lexical/link';
|
||||
import { type Transformer, TRANSFORMERS } from '@lexical/markdown';
|
||||
import { ListItemNode, ListNode } from '@lexical/list';
|
||||
import { HeadingNode, QuoteNode } from '@lexical/rich-text';
|
||||
import { type Klass, type LexicalNode } from 'lexical';
|
||||
|
||||
import { normalizeChatMessageLineEndings } from '@shared/utils/chatMessage';
|
||||
|
||||
const fencedCodePattern = /(^|\n)```/;
|
||||
const blockMarkdownPattern = /(^|\n)\s{0,3}(?:#{1,6}\s|>\s|[-*+]\s|\d+\.\s|\[[ xX]\]\s)/;
|
||||
const tablePattern = /(^|\n)\|.*\|\s*\n\|?[\s:-]+\|/;
|
||||
const linkPattern = /\[([^\]]+)]\(([^)]+)\)/;
|
||||
const inlineFormattingPattern =
|
||||
/(?:`[^`]+`|\*\*[^*]+\*\*|__[^_]+__|~~[^~]+~~|\*[^*\n]+\*|_[^_\n]+_)/;
|
||||
|
||||
export type MarkdownPasteReason =
|
||||
| 'fenced-code'
|
||||
| 'block-structure'
|
||||
| 'table'
|
||||
| 'link'
|
||||
| 'inline-format'
|
||||
| 'plain-text';
|
||||
|
||||
export interface MarkdownPasteInspection {
|
||||
normalizedText: string;
|
||||
shouldImportMarkdown: boolean;
|
||||
reason: MarkdownPasteReason;
|
||||
}
|
||||
|
||||
export const markdownEditorNamespace = 'eryx-markdown-composer';
|
||||
|
||||
export const markdownEditorNodes: ReadonlyArray<Klass<LexicalNode>> = [
|
||||
HeadingNode,
|
||||
QuoteNode,
|
||||
ListNode,
|
||||
ListItemNode,
|
||||
LinkNode,
|
||||
AutoLinkNode,
|
||||
CodeNode,
|
||||
];
|
||||
|
||||
export const markdownEditorTransformers: ReadonlyArray<Transformer> = TRANSFORMERS;
|
||||
|
||||
export function inspectMarkdownPaste(text: string): MarkdownPasteInspection {
|
||||
const normalizedText = normalizeChatMessageLineEndings(text);
|
||||
|
||||
if (fencedCodePattern.test(normalizedText)) {
|
||||
return {
|
||||
normalizedText,
|
||||
shouldImportMarkdown: true,
|
||||
reason: 'fenced-code',
|
||||
};
|
||||
}
|
||||
|
||||
if (blockMarkdownPattern.test(normalizedText)) {
|
||||
return {
|
||||
normalizedText,
|
||||
shouldImportMarkdown: true,
|
||||
reason: 'block-structure',
|
||||
};
|
||||
}
|
||||
|
||||
if (tablePattern.test(normalizedText)) {
|
||||
return {
|
||||
normalizedText,
|
||||
shouldImportMarkdown: true,
|
||||
reason: 'table',
|
||||
};
|
||||
}
|
||||
|
||||
if (linkPattern.test(normalizedText)) {
|
||||
return {
|
||||
normalizedText,
|
||||
shouldImportMarkdown: true,
|
||||
reason: 'link',
|
||||
};
|
||||
}
|
||||
|
||||
if (inlineFormattingPattern.test(normalizedText)) {
|
||||
return {
|
||||
normalizedText,
|
||||
shouldImportMarkdown: true,
|
||||
reason: 'inline-format',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
normalizedText,
|
||||
shouldImportMarkdown: false,
|
||||
reason: 'plain-text',
|
||||
};
|
||||
}
|
||||
|
||||
export function shouldImportMarkdownPaste(text: string): boolean {
|
||||
return inspectMarkdownPaste(text).shouldImportMarkdown;
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import type {
|
||||
SessionRunRecord,
|
||||
SessionRunStatus,
|
||||
} from '@shared/domain/runTimeline';
|
||||
import { buildMarkdownExcerpt } from '@shared/utils/markdownText';
|
||||
|
||||
export function formatRunTimestamp(isoDate: string): string {
|
||||
try {
|
||||
@@ -156,9 +157,7 @@ export function eventSortKey(event: RunTimelineEventRecord): number {
|
||||
|
||||
export function truncateContent(content: string | undefined, maxLength = 80): string | undefined {
|
||||
if (!content) return undefined;
|
||||
const singleLine = content.replace(/\n/g, ' ').trim();
|
||||
if (singleLine.length <= maxLength) return singleLine;
|
||||
return `${singleLine.slice(0, maxLength)}…`;
|
||||
return buildMarkdownExcerpt(content, maxLength);
|
||||
}
|
||||
|
||||
export function findLatestRun(runs: readonly SessionRunRecord[]): SessionRunRecord | undefined {
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
type ApprovalPolicy,
|
||||
validateApprovalPolicy,
|
||||
} from '@shared/domain/approval';
|
||||
import { buildMarkdownExcerpt } from '@shared/utils/markdownText';
|
||||
|
||||
export type OrchestrationMode =
|
||||
| 'single'
|
||||
@@ -1009,5 +1010,5 @@ export function buildSessionTitle(pattern: PatternDefinition, messages: ChatMess
|
||||
return pattern.name;
|
||||
}
|
||||
|
||||
return firstUserMessage.content.slice(0, 48).trim() || pattern.name;
|
||||
return buildMarkdownExcerpt(firstUserMessage.content, 48) ?? pattern.name;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
export function normalizeChatMessageLineEndings(value: string): string {
|
||||
return value.replace(/\r\n?/g, '\n');
|
||||
}
|
||||
|
||||
export function hasMeaningfulChatMessageContent(value: string): boolean {
|
||||
return normalizeChatMessageLineEndings(value).trim().length > 0;
|
||||
}
|
||||
|
||||
export function prepareChatMessageContent(value: string): string | undefined {
|
||||
const normalized = normalizeChatMessageLineEndings(value);
|
||||
return normalized.trim().length > 0 ? normalized : undefined;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { normalizeChatMessageLineEndings } from '@shared/utils/chatMessage';
|
||||
|
||||
const fencedCodeBlockPattern = /```[^\n]*\n([\s\S]*?)```/g;
|
||||
const inlineCodePattern = /`([^`]+)`/g;
|
||||
const imagePattern = /!\[([^\]]*)]\([^)]+\)/g;
|
||||
const linkPattern = /\[([^\]]+)]\([^)]+\)/g;
|
||||
const autoLinkPattern = /<((?:https?:\/\/|mailto:)[^>]+)>/g;
|
||||
const headingPattern = /^\s{0,3}#{1,6}\s+/gm;
|
||||
const blockquotePattern = /^\s{0,3}>\s?/gm;
|
||||
const listPattern = /^\s{0,3}(?:[-*+]|\d+\.)\s+/gm;
|
||||
const checklistPattern = /^\s*\[[ xX]\]\s+/gm;
|
||||
const setextHeadingPattern = /^[=-]{2,}\s*$/gm;
|
||||
const thematicBreakPattern = /^[ \t]{0,3}(?:[-*_][ \t]*){3,}$/gm;
|
||||
const tableSeparatorPattern = /^\s*\|?(?:\s*:?-{3,}:?\s*\|)+\s*:?-{3,}:?\s*\|?\s*$/gm;
|
||||
const emphasisPattern = /(\*\*|__|~~|\*|_)/g;
|
||||
const markdownEscapePattern = /\\([\\`*_{}\[\]()#+\-.!>])/g;
|
||||
|
||||
export function extractPlainTextFromMarkdown(markdown: string): string {
|
||||
const normalized = normalizeChatMessageLineEndings(markdown);
|
||||
|
||||
const plain = normalized
|
||||
.replace(fencedCodeBlockPattern, '$1')
|
||||
.replace(inlineCodePattern, '$1')
|
||||
.replace(imagePattern, '$1')
|
||||
.replace(linkPattern, '$1')
|
||||
.replace(autoLinkPattern, '$1')
|
||||
.replace(headingPattern, '')
|
||||
.replace(blockquotePattern, '')
|
||||
.replace(listPattern, '')
|
||||
.replace(checklistPattern, '')
|
||||
.replace(setextHeadingPattern, ' ')
|
||||
.replace(thematicBreakPattern, ' ')
|
||||
.replace(tableSeparatorPattern, ' ')
|
||||
.replace(/\|/g, ' ')
|
||||
.replace(emphasisPattern, '')
|
||||
.replace(markdownEscapePattern, '$1')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
|
||||
return plain;
|
||||
}
|
||||
|
||||
export function buildMarkdownExcerpt(markdown: string | undefined, maxLength = 80): string | undefined {
|
||||
if (!markdown) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const plainText = extractPlainTextFromMarkdown(markdown);
|
||||
if (!plainText) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (plainText.length <= maxLength) {
|
||||
return plainText;
|
||||
}
|
||||
|
||||
return `${plainText.slice(0, maxLength).trimEnd()}…`;
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
import { createHeadlessEditor } from '@lexical/headless';
|
||||
import { $convertFromMarkdownString, $convertToMarkdownString } from '@lexical/markdown';
|
||||
|
||||
import {
|
||||
inspectMarkdownPaste,
|
||||
markdownEditorNamespace,
|
||||
markdownEditorNodes,
|
||||
markdownEditorTransformers,
|
||||
} from '@renderer/lib/markdownEditor';
|
||||
|
||||
function roundTripMarkdown(markdown: string): string {
|
||||
const editor = createHeadlessEditor({
|
||||
namespace: markdownEditorNamespace,
|
||||
nodes: [...markdownEditorNodes],
|
||||
onError(error) {
|
||||
throw error;
|
||||
},
|
||||
});
|
||||
|
||||
editor.update(() => {
|
||||
$convertFromMarkdownString(markdown, [...markdownEditorTransformers], undefined, true, false);
|
||||
}, { discrete: true });
|
||||
|
||||
let result = '';
|
||||
editor.getEditorState().read(() => {
|
||||
result = $convertToMarkdownString([...markdownEditorTransformers], undefined, true);
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
describe('markdown editor contract', () => {
|
||||
test('round-trips markdown structures with the chosen node set', () => {
|
||||
const markdown = [
|
||||
'# Release notes',
|
||||
'',
|
||||
'- item one',
|
||||
'- item two',
|
||||
'',
|
||||
'> keep the markdown contract stable',
|
||||
'',
|
||||
'```ts',
|
||||
'const answer = 42;',
|
||||
'```',
|
||||
'',
|
||||
'[Docs](https://example.com)',
|
||||
].join('\n');
|
||||
|
||||
const roundTripped = roundTripMarkdown(markdown);
|
||||
|
||||
expect(roundTripped).toContain('# Release notes');
|
||||
expect(roundTripped).toContain('- item one');
|
||||
expect(roundTripped).toContain('```ts');
|
||||
expect(roundTripped).toContain('[Docs](https://example.com)');
|
||||
});
|
||||
|
||||
test('classifies fenced code as markdown paste', () => {
|
||||
expect(inspectMarkdownPaste('```ts\nconst answer = 42;\n```')).toEqual({
|
||||
normalizedText: '```ts\nconst answer = 42;\n```',
|
||||
shouldImportMarkdown: true,
|
||||
reason: 'fenced-code',
|
||||
});
|
||||
});
|
||||
|
||||
test('keeps plain prose on the plain-text path', () => {
|
||||
expect(inspectMarkdownPaste('Just a normal sentence about the release plan.')).toEqual({
|
||||
normalizedText: 'Just a normal sentence about the release plan.',
|
||||
shouldImportMarkdown: false,
|
||||
reason: 'plain-text',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -86,6 +86,7 @@ describe('run timeline formatting', () => {
|
||||
expect(truncateContent('Short content')).toBe('Short content');
|
||||
expect(truncateContent('A'.repeat(100), 80)).toBe('A'.repeat(80) + '…');
|
||||
expect(truncateContent('Line 1\nLine 2')).toBe('Line 1 Line 2');
|
||||
expect(truncateContent('**Bold** [Docs](https://example.com)')).toBe('Bold Docs');
|
||||
});
|
||||
|
||||
test('collapses consecutive thinking events from the same agent', () => {
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
|
||||
import {
|
||||
hasMeaningfulChatMessageContent,
|
||||
normalizeChatMessageLineEndings,
|
||||
prepareChatMessageContent,
|
||||
} from '@shared/utils/chatMessage';
|
||||
|
||||
describe('chat message helpers', () => {
|
||||
test('normalizes Windows line endings without trimming meaningful markdown', () => {
|
||||
expect(
|
||||
prepareChatMessageContent('```ts\r\n const answer = 42;\r\n```\r\n'),
|
||||
).toBe('```ts\n const answer = 42;\n```\n');
|
||||
});
|
||||
|
||||
test('treats whitespace-only input as empty', () => {
|
||||
expect(hasMeaningfulChatMessageContent(' \r\n\t ')).toBe(false);
|
||||
expect(prepareChatMessageContent(' \r\n\t ')).toBeUndefined();
|
||||
});
|
||||
|
||||
test('normalizes mixed newline sequences consistently', () => {
|
||||
expect(normalizeChatMessageLineEndings('line 1\r\nline 2\rline 3')).toBe(
|
||||
'line 1\nline 2\nline 3',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
|
||||
import { buildMarkdownExcerpt, extractPlainTextFromMarkdown } from '@shared/utils/markdownText';
|
||||
|
||||
describe('markdown text helpers', () => {
|
||||
test('extracts readable text from common markdown syntax', () => {
|
||||
expect(
|
||||
extractPlainTextFromMarkdown('# Title\n\n**Bold** [Docs](https://example.com) and `code`.'),
|
||||
).toBe('Title Bold Docs and code.');
|
||||
});
|
||||
|
||||
test('turns fenced code blocks into readable excerpts', () => {
|
||||
expect(buildMarkdownExcerpt('```ts\nconst answer = 42;\n```', 48)).toBe('const answer = 42;');
|
||||
});
|
||||
|
||||
test('truncates after stripping markdown syntax', () => {
|
||||
expect(buildMarkdownExcerpt('**Alpha** beta gamma delta epsilon zeta eta theta', 18)).toBe(
|
||||
'Alpha beta gamma d…',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -121,6 +121,23 @@ describe('session title helpers', () => {
|
||||
]),
|
||||
).toBe('Release readiness review');
|
||||
});
|
||||
|
||||
test('builds auto titles from markdown-heavy first messages', () => {
|
||||
const pattern = createPattern();
|
||||
const session = createSession();
|
||||
|
||||
expect(
|
||||
resolveSessionTitle(session, pattern, [
|
||||
{
|
||||
id: 'msg-1',
|
||||
role: 'user',
|
||||
authorName: 'You',
|
||||
content: '```ts\nconst answer = 42;\n```',
|
||||
createdAt: '2026-03-23T00:00:00.000Z',
|
||||
},
|
||||
]),
|
||||
).toBe('const answer = 42;');
|
||||
});
|
||||
});
|
||||
|
||||
describe('session tooling helpers', () => {
|
||||
|
||||
Reference in New Issue
Block a user