diff --git a/bun.lock b/bun.lock index e918628a..c5ba3d23 100644 --- a/bun.lock +++ b/bun.lock @@ -5,12 +5,32 @@ "": { "name": "openchamber-monorepo", "dependencies": { + "@codemirror/autocomplete": "^6.20.0", + "@codemirror/commands": "^6.10.1", + "@codemirror/highlight": "^0.19.8", + "@codemirror/history": "^0.19.2", + "@codemirror/lang-css": "^6.3.1", + "@codemirror/lang-html": "^6.4.11", + "@codemirror/lang-javascript": "^6.2.4", + "@codemirror/lang-json": "^6.0.2", + "@codemirror/lang-markdown": "^6.5.0", + "@codemirror/lang-python": "^6.2.1", + "@codemirror/lang-rust": "^6.0.2", + "@codemirror/lang-sql": "^6.10.0", + "@codemirror/lang-xml": "^6.1.0", + "@codemirror/lang-yaml": "^6.1.2", + "@codemirror/language": "^6.12.1", + "@codemirror/lint": "^6.9.2", + "@codemirror/search": "^6.6.0", + "@codemirror/state": "^6.5.4", + "@codemirror/view": "^6.39.11", "@fontsource/ibm-plex-mono": "^5.2.7", "@fontsource/ibm-plex-sans": "^5.1.1", "@heroui/scroll-shadow": "^2.3.18", "@heroui/system": "^2.4.23", "@heroui/theme": "^2.4.23", "@ibm/plex": "^6.4.1", + "@lezer/highlight": "^1.2.3", "@opencode-ai/sdk": "^1.1.19", "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-dialog": "^1.1.15", @@ -74,7 +94,7 @@ }, "packages/desktop": { "name": "@openchamber/desktop", - "version": "1.4.8", + "version": "1.5.2", "dependencies": { "@openchamber/ui": "workspace:*", "@tauri-apps/plugin-notification": "^2.3.3", @@ -97,14 +117,36 @@ }, "packages/ui": { "name": "@openchamber/ui", - "version": "1.4.8", + "version": "1.5.2", "dependencies": { + "@codemirror/autocomplete": "^6.20.0", + "@codemirror/commands": "^6.10.1", + "@codemirror/highlight": "^0.19.8", + "@codemirror/history": "^0.19.2", + "@codemirror/lang-css": "^6.3.1", + "@codemirror/lang-html": "^6.4.11", + "@codemirror/lang-javascript": "^6.2.4", + "@codemirror/lang-json": "^6.0.2", + "@codemirror/lang-markdown": "^6.5.0", + "@codemirror/lang-python": "^6.2.1", + "@codemirror/lang-rust": "^6.0.2", + "@codemirror/lang-sql": "^6.10.0", + "@codemirror/lang-xml": "^6.1.0", + "@codemirror/lang-yaml": "^6.1.2", + "@codemirror/language": "^6.12.1", + "@codemirror/language-data": "^6.5.2", + "@codemirror/legacy-modes": "^6.5.2", + "@codemirror/lint": "^6.9.2", + "@codemirror/search": "^6.6.0", + "@codemirror/state": "^6.5.4", + "@codemirror/view": "^6.39.11", "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", "@fontsource/ibm-plex-mono": "^5.2.7", "@fontsource/ibm-plex-sans": "^5.1.1", "@ibm/plex": "^6.4.1", + "@lezer/highlight": "^1.2.3", "@opencode-ai/sdk": "^1.1.19", "@pierre/diffs": "^1.0.5", "@radix-ui/react-collapsible": "^1.1.12", @@ -169,7 +211,7 @@ }, "packages/vscode": { "name": "openchamber", - "version": "1.4.8", + "version": "1.5.2", "dependencies": { "@openchamber/ui": "workspace:*", "@opencode-ai/sdk": "^1.1.19", @@ -192,7 +234,7 @@ }, "packages/web": { "name": "@openchamber/web", - "version": "1.4.8", + "version": "1.5.2", "bin": { "openchamber": "./bin/cli.js", }, @@ -347,6 +389,74 @@ "@chevrotain/utils": ["@chevrotain/utils@11.0.3", "", {}, "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ=="], + "@codemirror/autocomplete": ["@codemirror/autocomplete@6.20.0", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0" } }, "sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg=="], + + "@codemirror/commands": ["@codemirror/commands@6.10.1", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.4.0", "@codemirror/view": "^6.27.0", "@lezer/common": "^1.1.0" } }, "sha512-uWDWFypNdQmz2y1LaNJzK7fL7TYKLeUAU0npEC685OKTF3KcQ2Vu3klIM78D7I6wGhktme0lh3CuQLv0ZCrD9Q=="], + + "@codemirror/highlight": ["@codemirror/highlight@0.19.8", "", { "dependencies": { "@codemirror/language": "^0.19.0", "@codemirror/rangeset": "^0.19.0", "@codemirror/state": "^0.19.3", "@codemirror/view": "^0.19.39", "@lezer/common": "^0.15.0", "style-mod": "^4.0.0" } }, "sha512-v/lzuHjrYR8MN2mEJcUD6fHSTXXli9C1XGYpr+ElV6fLBIUhMTNKR3qThp611xuWfXfwDxeL7ppcbkM/MzPV3A=="], + + "@codemirror/history": ["@codemirror/history@0.19.2", "", { "dependencies": { "@codemirror/state": "^0.19.2", "@codemirror/view": "^0.19.0" } }, "sha512-unhP4t3N2smzmHoo/Yio6ueWi+il8gm9VKrvi6wlcdGH5fOfVDNkmjHQ495SiR+EdOG35+3iNebSPYww0vN7ow=="], + + "@codemirror/lang-angular": ["@codemirror/lang-angular@0.1.4", "", { "dependencies": { "@codemirror/lang-html": "^6.0.0", "@codemirror/lang-javascript": "^6.1.2", "@codemirror/language": "^6.0.0", "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.3.3" } }, "sha512-oap+gsltb/fzdlTQWD6BFF4bSLKcDnlxDsLdePiJpCVNKWXSTAbiiQeYI3UmES+BLAdkmIC1WjyztC1pi/bX4g=="], + + "@codemirror/lang-cpp": ["@codemirror/lang-cpp@6.0.3", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@lezer/cpp": "^1.0.0" } }, "sha512-URM26M3vunFFn9/sm6rzqrBzDgfWuDixp85uTY49wKudToc2jTHUrKIGGKs+QWND+YLofNNZpxcNGRynFJfvgA=="], + + "@codemirror/lang-css": ["@codemirror/lang-css@6.3.1", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@lezer/common": "^1.0.2", "@lezer/css": "^1.1.7" } }, "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg=="], + + "@codemirror/lang-go": ["@codemirror/lang-go@6.0.1", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.6.0", "@codemirror/state": "^6.0.0", "@lezer/common": "^1.0.0", "@lezer/go": "^1.0.0" } }, "sha512-7fNvbyNylvqCphW9HD6WFnRpcDjr+KXX/FgqXy5H5ZS0eC5edDljukm/yNgYkwTsgp2busdod50AOTIy6Jikfg=="], + + "@codemirror/lang-html": ["@codemirror/lang-html@6.4.11", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/lang-css": "^6.0.0", "@codemirror/lang-javascript": "^6.0.0", "@codemirror/language": "^6.4.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0", "@lezer/css": "^1.1.0", "@lezer/html": "^1.3.12" } }, "sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw=="], + + "@codemirror/lang-java": ["@codemirror/lang-java@6.0.2", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@lezer/java": "^1.0.0" } }, "sha512-m5Nt1mQ/cznJY7tMfQTJchmrjdjQ71IDs+55d1GAa8DGaB8JXWsVCkVT284C3RTASaY43YknrK2X3hPO/J3MOQ=="], + + "@codemirror/lang-javascript": ["@codemirror/lang-javascript@6.2.4", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.6.0", "@codemirror/lint": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0", "@lezer/javascript": "^1.0.0" } }, "sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA=="], + + "@codemirror/lang-jinja": ["@codemirror/lang-jinja@6.0.0", "", { "dependencies": { "@codemirror/lang-html": "^6.0.0", "@codemirror/language": "^6.0.0", "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.2.0", "@lezer/lr": "^1.4.0" } }, "sha512-47MFmRcR8UAxd8DReVgj7WJN1WSAMT7OJnewwugZM4XiHWkOjgJQqvEM1NpMj9ALMPyxmlziEI1opH9IaEvmaw=="], + + "@codemirror/lang-json": ["@codemirror/lang-json@6.0.2", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@lezer/json": "^1.0.0" } }, "sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ=="], + + "@codemirror/lang-less": ["@codemirror/lang-less@6.0.2", "", { "dependencies": { "@codemirror/lang-css": "^6.2.0", "@codemirror/language": "^6.0.0", "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-EYdQTG22V+KUUk8Qq582g7FMnCZeEHsyuOJisHRft/mQ+ZSZ2w51NupvDUHiqtsOy7It5cHLPGfHQLpMh9bqpQ=="], + + "@codemirror/lang-liquid": ["@codemirror/lang-liquid@6.3.1", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/lang-html": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", "@lezer/common": "^1.0.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.3.1" } }, "sha512-S/jE/D7iij2Pu70AC65ME6AYWxOOcX20cSJvaPgY5w7m2sfxsArAcUAuUgm/CZCVmqoi9KiOlS7gj/gyLipABw=="], + + "@codemirror/lang-markdown": ["@codemirror/lang-markdown@6.5.0", "", { "dependencies": { "@codemirror/autocomplete": "^6.7.1", "@codemirror/lang-html": "^6.0.0", "@codemirror/language": "^6.3.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", "@lezer/common": "^1.2.1", "@lezer/markdown": "^1.0.0" } }, "sha512-0K40bZ35jpHya6FriukbgaleaqzBLZfOh7HuzqbMxBXkbYMJDxfF39c23xOgxFezR+3G+tR2/Mup+Xk865OMvw=="], + + "@codemirror/lang-php": ["@codemirror/lang-php@6.0.2", "", { "dependencies": { "@codemirror/lang-html": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@lezer/common": "^1.0.0", "@lezer/php": "^1.0.0" } }, "sha512-ZKy2v1n8Fc8oEXj0Th0PUMXzQJ0AIR6TaZU+PbDHExFwdu+guzOA4jmCHS1Nz4vbFezwD7LyBdDnddSJeScMCA=="], + + "@codemirror/lang-python": ["@codemirror/lang-python@6.2.1", "", { "dependencies": { "@codemirror/autocomplete": "^6.3.2", "@codemirror/language": "^6.8.0", "@codemirror/state": "^6.0.0", "@lezer/common": "^1.2.1", "@lezer/python": "^1.1.4" } }, "sha512-IRjC8RUBhn9mGR9ywecNhB51yePWCGgvHfY1lWN/Mrp3cKuHr0isDKia+9HnvhiWNnMpbGhWrkhuWOc09exRyw=="], + + "@codemirror/lang-rust": ["@codemirror/lang-rust@6.0.2", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@lezer/rust": "^1.0.0" } }, "sha512-EZaGjCUegtiU7kSMvOfEZpaCReowEf3yNidYu7+vfuGTm9ow4mthAparY5hisJqOHmJowVH3Upu+eJlUji6qqA=="], + + "@codemirror/lang-sass": ["@codemirror/lang-sass@6.0.2", "", { "dependencies": { "@codemirror/lang-css": "^6.2.0", "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@lezer/common": "^1.0.2", "@lezer/sass": "^1.0.0" } }, "sha512-l/bdzIABvnTo1nzdY6U+kPAC51czYQcOErfzQ9zSm9D8GmNPD0WTW8st/CJwBTPLO8jlrbyvlSEcN20dc4iL0Q=="], + + "@codemirror/lang-sql": ["@codemirror/lang-sql@6.10.0", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-6ayPkEd/yRw0XKBx5uAiToSgGECo/GY2NoJIHXIIQh1EVwLuKoU8BP/qK0qH5NLXAbtJRLuT73hx7P9X34iO4w=="], + + "@codemirror/lang-vue": ["@codemirror/lang-vue@0.1.3", "", { "dependencies": { "@codemirror/lang-html": "^6.0.0", "@codemirror/lang-javascript": "^6.1.2", "@codemirror/language": "^6.0.0", "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.3.1" } }, "sha512-QSKdtYTDRhEHCfo5zOShzxCmqKJvgGrZwDQSdbvCRJ5pRLWBS7pD/8e/tH44aVQT6FKm0t6RVNoSUWHOI5vNug=="], + + "@codemirror/lang-wast": ["@codemirror/lang-wast@6.0.2", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-Imi2KTpVGm7TKuUkqyJ5NRmeFWF7aMpNiwHnLQe0x9kmrxElndyH0K6H/gXtWwY6UshMRAhpENsgfpSwsgmC6Q=="], + + "@codemirror/lang-xml": ["@codemirror/lang-xml@6.1.0", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.4.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", "@lezer/common": "^1.0.0", "@lezer/xml": "^1.0.0" } }, "sha512-3z0blhicHLfwi2UgkZYRPioSgVTo9PV5GP5ducFH6FaHy0IAJRg+ixj5gTR1gnT/glAIC8xv4w2VL1LoZfs+Jg=="], + + "@codemirror/lang-yaml": ["@codemirror/lang-yaml@6.1.2", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.2.0", "@lezer/lr": "^1.0.0", "@lezer/yaml": "^1.0.0" } }, "sha512-dxrfG8w5Ce/QbT7YID7mWZFKhdhsaTNOYjOkSIMt1qmC4VQnXSDSYVHHHn8k6kJUfIhtLo8t1JJgltlxWdsITw=="], + + "@codemirror/language": ["@codemirror/language@6.12.1", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.23.0", "@lezer/common": "^1.5.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0", "style-mod": "^4.0.0" } }, "sha512-Fa6xkSiuGKc8XC8Cn96T+TQHYj4ZZ7RdFmXA3i9xe/3hLHfwPZdM+dqfX0Cp0zQklBKhVD8Yzc8LS45rkqcwpQ=="], + + "@codemirror/language-data": ["@codemirror/language-data@6.5.2", "", { "dependencies": { "@codemirror/lang-angular": "^0.1.0", "@codemirror/lang-cpp": "^6.0.0", "@codemirror/lang-css": "^6.0.0", "@codemirror/lang-go": "^6.0.0", "@codemirror/lang-html": "^6.0.0", "@codemirror/lang-java": "^6.0.0", "@codemirror/lang-javascript": "^6.0.0", "@codemirror/lang-jinja": "^6.0.0", "@codemirror/lang-json": "^6.0.0", "@codemirror/lang-less": "^6.0.0", "@codemirror/lang-liquid": "^6.0.0", "@codemirror/lang-markdown": "^6.0.0", "@codemirror/lang-php": "^6.0.0", "@codemirror/lang-python": "^6.0.0", "@codemirror/lang-rust": "^6.0.0", "@codemirror/lang-sass": "^6.0.0", "@codemirror/lang-sql": "^6.0.0", "@codemirror/lang-vue": "^0.1.1", "@codemirror/lang-wast": "^6.0.0", "@codemirror/lang-xml": "^6.0.0", "@codemirror/lang-yaml": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/legacy-modes": "^6.4.0" } }, "sha512-CPkWBKrNS8stYbEU5kwBwTf3JB1kghlbh4FSAwzGW2TEscdeHHH4FGysREW86Mqnj3Qn09s0/6Ea/TutmoTobg=="], + + "@codemirror/legacy-modes": ["@codemirror/legacy-modes@6.5.2", "", { "dependencies": { "@codemirror/language": "^6.0.0" } }, "sha512-/jJbwSTazlQEDOQw2FJ8LEEKVS72pU0lx6oM54kGpL8t/NJ2Jda3CZ4pcltiKTdqYSRk3ug1B3pil1gsjA6+8Q=="], + + "@codemirror/lint": ["@codemirror/lint@6.9.2", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.35.0", "crelt": "^1.0.5" } }, "sha512-sv3DylBiIyi+xKwRCJAAsBZZZWo82shJ/RTMymLabAdtbkV5cSKwWDeCgtUq3v8flTaXS2y1kKkICuRYtUswyQ=="], + + "@codemirror/rangeset": ["@codemirror/rangeset@0.19.9", "", { "dependencies": { "@codemirror/state": "^0.19.0" } }, "sha512-V8YUuOvK+ew87Xem+71nKcqu1SXd5QROMRLMS/ljT5/3MCxtgrRie1Cvild0G/Z2f1fpWxzX78V0U4jjXBorBQ=="], + + "@codemirror/search": ["@codemirror/search@6.6.0", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.37.0", "crelt": "^1.0.5" } }, "sha512-koFuNXcDvyyotWcgOnZGmY7LZqEOXZaaxD/j6n18TCLx2/9HieZJ5H6hs1g8FiRxBD0DNfs0nXn17g872RmYdw=="], + + "@codemirror/state": ["@codemirror/state@6.5.4", "", { "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } }, "sha512-8y7xqG/hpB53l25CIoit9/ngxdfoG+fx+V3SHBrinnhOtLvKHRyAJJuHzkWrR4YXXLX8eXBsejgAAxHUOdW1yw=="], + + "@codemirror/text": ["@codemirror/text@0.19.6", "", {}, "sha512-T9jnREMIygx+TPC1bOuepz18maGq/92q2a+n4qTqObKwvNMg+8cMTslb8yxeEDEq7S3kpgGWxgO1UWbQRij0dA=="], + + "@codemirror/view": ["@codemirror/view@6.39.11", "", { "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-bWdeR8gWM87l4DB/kYSF9A+dVackzDb/V56Tq7QVrQ7rn86W0rgZFtlL3g3pem6AeGcb9NQNoy3ao4WpW4h5tQ=="], + "@develar/schema-utils": ["@develar/schema-utils@2.6.5", "", { "dependencies": { "ajv": "^6.12.0", "ajv-keywords": "^3.4.1" } }, "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig=="], "@dnd-kit/accessibility": ["@dnd-kit/accessibility@3.1.1", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw=="], @@ -519,10 +629,46 @@ "@kwsites/promise-deferred": ["@kwsites/promise-deferred@1.1.1", "", {}, "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw=="], + "@lezer/common": ["@lezer/common@1.5.0", "", {}, "sha512-PNGcolp9hr4PJdXR4ix7XtixDrClScvtSCYW3rQG106oVMOOI+jFb+0+J3mbeL/53g1Zd6s0kJzaw6Ri68GmAA=="], + + "@lezer/cpp": ["@lezer/cpp@1.1.5", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-DIhSXmYtJKLehrjzDFN+2cPt547ySQ41nA8yqcDf/GxMc+YM736xqltFkvADL2M0VebU5I+3+4ks2Vv+Kyq3Aw=="], + + "@lezer/css": ["@lezer/css@1.3.0", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.3.0" } }, "sha512-pBL7hup88KbI7hXnZV3PQsn43DHy6TWyzuyk2AO9UyoXcDltvIdqWKE1dLL/45JVZ+YZkHe1WVHqO6wugZZWcw=="], + + "@lezer/go": ["@lezer/go@1.0.1", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.3.0" } }, "sha512-xToRsYxwsgJNHTgNdStpcvmbVuKxTapV0dM0wey1geMMRc9aggoVyKgzYp41D2/vVOx+Ii4hmE206kvxIXBVXQ=="], + + "@lezer/highlight": ["@lezer/highlight@1.2.3", "", { "dependencies": { "@lezer/common": "^1.3.0" } }, "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g=="], + + "@lezer/html": ["@lezer/html@1.3.13", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg=="], + + "@lezer/java": ["@lezer/java@1.1.3", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-yHquUfujwg6Yu4Fd1GNHCvidIvJwi/1Xu2DaKl/pfWIA2c1oXkVvawH3NyXhCaFx4OdlYBVX5wvz2f7Aoa/4Xw=="], + + "@lezer/javascript": ["@lezer/javascript@1.5.4", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.1.3", "@lezer/lr": "^1.3.0" } }, "sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA=="], + + "@lezer/json": ["@lezer/json@1.0.3", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ=="], + + "@lezer/lr": ["@lezer/lr@1.4.7", "", { "dependencies": { "@lezer/common": "^1.0.0" } }, "sha512-wNIFWdSUfX9Jc6ePMzxSPVgTVB4EOfDIwLQLWASyiUdHKaMsiilj9bYiGkGQCKVodd0x6bgQCV207PILGFCF9Q=="], + + "@lezer/markdown": ["@lezer/markdown@1.6.3", "", { "dependencies": { "@lezer/common": "^1.5.0", "@lezer/highlight": "^1.0.0" } }, "sha512-jpGm5Ps+XErS+xA4urw7ogEGkeZOahVQF21Z6oECF0sj+2liwZopd2+I8uH5I/vZsRuuze3OxBREIANLf6KKUw=="], + + "@lezer/php": ["@lezer/php@1.0.5", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.1.0" } }, "sha512-W7asp9DhM6q0W6DYNwIkLSKOvxlXRrif+UXBMxzsJUuqmhE7oVU+gS3THO4S/Puh7Xzgm858UNaFi6dxTP8dJA=="], + + "@lezer/python": ["@lezer/python@1.1.18", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-31FiUrU7z9+d/ElGQLJFXl+dKOdx0jALlP3KEOsGTex8mvj+SoE1FgItcHWK/axkxCHGUSpqIHt6JAWfWu9Rhg=="], + + "@lezer/rust": ["@lezer/rust@1.0.2", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-Lz5sIPBdF2FUXcWeCu1//ojFAZqzTQNRga0aYv6dYXqJqPfMdCAI0NzajWUd4Xijj1IKJLtjoXRPMvTKWBcqKg=="], + + "@lezer/sass": ["@lezer/sass@1.1.0", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-3mMGdCTUZ/84ArHOuXWQr37pnf7f+Nw9ycPUeKX+wu19b7pSMcZGLbaXwvD2APMBDOGxPmpK/O6S1v1EvLoqgQ=="], + + "@lezer/xml": ["@lezer/xml@1.0.6", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-CdDwirL0OEaStFue/66ZmFSeppuL6Dwjlk8qk153mSQwiSH/Dlri4GNymrNWnUmPl2Um7QfV1FO9KFUyX3Twww=="], + + "@lezer/yaml": ["@lezer/yaml@1.0.3", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.4.0" } }, "sha512-GuBLekbw9jDBDhGur82nuwkxKQ+a3W5H0GfaAthDXcAu+XdpS43VlnxA9E9hllkpSP5ellRDKjLLj7Lu9Wr6xA=="], + "@malept/cross-spawn-promise": ["@malept/cross-spawn-promise@1.1.1", "", { "dependencies": { "cross-spawn": "^7.0.1" } }, "sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ=="], "@malept/flatpak-bundler": ["@malept/flatpak-bundler@0.4.0", "", { "dependencies": { "debug": "^4.1.1", "fs-extra": "^9.0.0", "lodash": "^4.17.15", "tmp-promise": "^3.0.2" } }, "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q=="], + "@marijn/find-cluster-break": ["@marijn/find-cluster-break@1.0.2", "", {}, "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g=="], + "@mermaid-js/parser": ["@mermaid-js/parser@0.6.3", "", { "dependencies": { "langium": "3.3.1" } }, "sha512-lnjOhe7zyHjc+If7yT4zoedx2vo4sHaTmtkl1+or8BRTnCtDmcTpAjpzDSfCZrshM5bCoz0GyidzadJAH1xobA=="], "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], @@ -1207,6 +1353,8 @@ "crc32-stream": ["crc32-stream@4.0.3", "", { "dependencies": { "crc-32": "^1.2.0", "readable-stream": "^3.4.0" } }, "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw=="], + "crelt": ["crelt@1.0.6", "", {}, "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="], + "cross-env": ["cross-env@7.0.3", "", { "dependencies": { "cross-spawn": "^7.0.1" }, "bin": { "cross-env": "src/bin/cross-env.js", "cross-env-shell": "src/bin/cross-env-shell.js" } }, "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw=="], "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], @@ -2357,6 +2505,8 @@ "stubborn-utils": ["stubborn-utils@1.0.2", "", {}, "sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg=="], + "style-mod": ["style-mod@4.1.3", "", {}, "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ=="], + "style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="], "style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="], @@ -2525,6 +2675,8 @@ "vscode-uri": ["vscode-uri@3.0.8", "", {}, "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw=="], + "w3c-keyname": ["w3c-keyname@2.2.8", "", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="], + "web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="], "whatwg-encoding": ["whatwg-encoding@3.1.1", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ=="], @@ -2587,6 +2739,20 @@ "@chevrotain/gast/lodash-es": ["lodash-es@4.17.21", "", {}, "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="], + "@codemirror/highlight/@codemirror/language": ["@codemirror/language@0.19.10", "", { "dependencies": { "@codemirror/state": "^0.19.0", "@codemirror/text": "^0.19.0", "@codemirror/view": "^0.19.0", "@lezer/common": "^0.15.5", "@lezer/lr": "^0.15.0" } }, "sha512-yA0DZ3RYn2CqAAGW62VrU8c4YxscMQn45y/I9sjBlqB1e2OTQLg4CCkMBuMSLXk4xaqjlsgazeOQWaJQOKfV8Q=="], + + "@codemirror/highlight/@codemirror/state": ["@codemirror/state@0.19.9", "", { "dependencies": { "@codemirror/text": "^0.19.0" } }, "sha512-psOzDolKTZkx4CgUqhBQ8T8gBc0xN5z4gzed109aF6x7D7umpDRoimacI/O6d9UGuyl4eYuDCZmDFr2Rq7aGOw=="], + + "@codemirror/highlight/@codemirror/view": ["@codemirror/view@0.19.48", "", { "dependencies": { "@codemirror/rangeset": "^0.19.5", "@codemirror/state": "^0.19.3", "@codemirror/text": "^0.19.0", "style-mod": "^4.0.0", "w3c-keyname": "^2.2.4" } }, "sha512-0eg7D2Nz4S8/caetCTz61rK0tkHI17V/d15Jy0kLOT8dTLGGNJUponDnW28h2B6bERmPlVHKh8MJIr5OCp1nGw=="], + + "@codemirror/highlight/@lezer/common": ["@lezer/common@0.15.12", "", {}, "sha512-edfwCxNLnzq5pBA/yaIhwJ3U3Kz8VAUOTRg0hhxaizaI1N+qxV7EXDv/kLCkLeq2RzSFvxexlaj5Mzfn2kY0Ig=="], + + "@codemirror/history/@codemirror/state": ["@codemirror/state@0.19.9", "", { "dependencies": { "@codemirror/text": "^0.19.0" } }, "sha512-psOzDolKTZkx4CgUqhBQ8T8gBc0xN5z4gzed109aF6x7D7umpDRoimacI/O6d9UGuyl4eYuDCZmDFr2Rq7aGOw=="], + + "@codemirror/history/@codemirror/view": ["@codemirror/view@0.19.48", "", { "dependencies": { "@codemirror/rangeset": "^0.19.5", "@codemirror/state": "^0.19.3", "@codemirror/text": "^0.19.0", "style-mod": "^4.0.0", "w3c-keyname": "^2.2.4" } }, "sha512-0eg7D2Nz4S8/caetCTz61rK0tkHI17V/d15Jy0kLOT8dTLGGNJUponDnW28h2B6bERmPlVHKh8MJIr5OCp1nGw=="], + + "@codemirror/rangeset/@codemirror/state": ["@codemirror/state@0.19.9", "", { "dependencies": { "@codemirror/text": "^0.19.0" } }, "sha512-psOzDolKTZkx4CgUqhBQ8T8gBc0xN5z4gzed109aF6x7D7umpDRoimacI/O6d9UGuyl4eYuDCZmDFr2Rq7aGOw=="], + "@electron/asar/commander": ["commander@5.1.0", "", {}, "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg=="], "@electron/asar/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], @@ -2831,6 +2997,8 @@ "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + "@codemirror/highlight/@codemirror/language/@lezer/lr": ["@lezer/lr@0.15.8", "", { "dependencies": { "@lezer/common": "^0.15.0" } }, "sha512-bM6oE6VQZ6hIFxDNKk8bKPa14hqFrV07J/vHGOeiAbJReIaQXmkVb6xQu4MR+JBTLa5arGRyAAjJe1qaQt3Uvg=="], + "@electron/get/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], "@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], diff --git a/package.json b/package.json index 8af42c21..4e2b9239 100644 --- a/package.json +++ b/package.json @@ -58,12 +58,32 @@ "release:test:arm": "./scripts/test-release-build.sh aarch64" }, "dependencies": { + "@codemirror/autocomplete": "^6.20.0", + "@codemirror/commands": "^6.10.1", + "@codemirror/highlight": "^0.19.8", + "@codemirror/history": "^0.19.2", + "@codemirror/lang-css": "^6.3.1", + "@codemirror/lang-html": "^6.4.11", + "@codemirror/lang-javascript": "^6.2.4", + "@codemirror/lang-json": "^6.0.2", + "@codemirror/lang-markdown": "^6.5.0", + "@codemirror/lang-python": "^6.2.1", + "@codemirror/lang-rust": "^6.0.2", + "@codemirror/lang-sql": "^6.10.0", + "@codemirror/lang-xml": "^6.1.0", + "@codemirror/lang-yaml": "^6.1.2", + "@codemirror/language": "^6.12.1", + "@codemirror/lint": "^6.9.2", + "@codemirror/search": "^6.6.0", + "@codemirror/state": "^6.5.4", + "@codemirror/view": "^6.39.11", "@fontsource/ibm-plex-mono": "^5.2.7", "@fontsource/ibm-plex-sans": "^5.1.1", "@heroui/scroll-shadow": "^2.3.18", "@heroui/system": "^2.4.23", "@heroui/theme": "^2.4.23", "@ibm/plex": "^6.4.1", + "@lezer/highlight": "^1.2.3", "@opencode-ai/sdk": "^1.1.19", "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-dialog": "^1.1.15", @@ -76,16 +96,16 @@ "@radix-ui/react-tooltip": "^1.2.8", "@remixicon/react": "^4.7.0", "@types/react-syntax-highlighter": "^15.5.13", - "ghostty-web": "0.3.0", + "bun-pty": "^0.4.5", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", "electron-context-menu": "^4.1.1", "electron-store": "^11.0.2", "express": "^5.1.0", + "ghostty-web": "0.3.0", "http-proxy-middleware": "^3.0.5", "next-themes": "^0.4.6", - "bun-pty": "^0.4.5", "node-pty": "^1.1.0", "react": "^19.1.1", "react-dom": "^19.1.1", diff --git a/packages/desktop/src-tauri/Cargo.lock b/packages/desktop/src-tauri/Cargo.lock index 044dfa27..9b8d0631 100644 --- a/packages/desktop/src-tauri/Cargo.lock +++ b/packages/desktop/src-tauri/Cargo.lock @@ -2977,7 +2977,7 @@ dependencies = [ [[package]] name = "openchamber-desktop" -version = "1.5.1" +version = "1.5.2" dependencies = [ "anyhow", "axum", diff --git a/packages/ui/package.json b/packages/ui/package.json index eacbba14..d392f1c9 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -11,12 +11,34 @@ "lint": "eslint \"./src/**/*.{ts,tsx}\" --config ../../eslint.config.js" }, "dependencies": { + "@codemirror/autocomplete": "^6.20.0", + "@codemirror/commands": "^6.10.1", + "@codemirror/highlight": "^0.19.8", + "@codemirror/history": "^0.19.2", + "@codemirror/lang-css": "^6.3.1", + "@codemirror/lang-html": "^6.4.11", + "@codemirror/lang-javascript": "^6.2.4", + "@codemirror/lang-json": "^6.0.2", + "@codemirror/lang-markdown": "^6.5.0", + "@codemirror/lang-python": "^6.2.1", + "@codemirror/lang-rust": "^6.0.2", + "@codemirror/lang-sql": "^6.10.0", + "@codemirror/lang-xml": "^6.1.0", + "@codemirror/lang-yaml": "^6.1.2", + "@codemirror/language": "^6.12.1", + "@codemirror/language-data": "^6.5.2", + "@codemirror/legacy-modes": "^6.5.2", + "@codemirror/lint": "^6.9.2", + "@codemirror/search": "^6.6.0", + "@codemirror/state": "^6.5.4", + "@codemirror/view": "^6.39.11", "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", "@fontsource/ibm-plex-mono": "^5.2.7", "@fontsource/ibm-plex-sans": "^5.1.1", "@ibm/plex": "^6.4.1", + "@lezer/highlight": "^1.2.3", "@opencode-ai/sdk": "^1.1.19", "@pierre/diffs": "^1.0.5", "@radix-ui/react-collapsible": "^1.1.12", diff --git a/packages/ui/src/components/ui/CodeMirrorEditor.tsx b/packages/ui/src/components/ui/CodeMirrorEditor.tsx new file mode 100644 index 00000000..88cd934f --- /dev/null +++ b/packages/ui/src/components/ui/CodeMirrorEditor.tsx @@ -0,0 +1,152 @@ +import React from 'react'; + +import type { Extension } from '@codemirror/state'; +import { Compartment, EditorState, RangeSetBuilder } from '@codemirror/state'; +import { Decoration, EditorView, ViewPlugin, gutters, keymap, lineNumbers } from '@codemirror/view'; +import { defaultKeymap, indentWithTab, history, historyKeymap } from '@codemirror/commands'; +import { indentUnit } from '@codemirror/language'; + +import { cn } from '@/lib/utils'; + +type CodeMirrorEditorProps = { + value: string; + onChange: (value: string) => void; + extensions?: Extension[]; + className?: string; + readOnly?: boolean; + lineNumbersConfig?: Parameters[0]; + highlightLines?: { start: number; end: number }; +}; + +const lineNumbersCompartment = new Compartment(); +const editableCompartment = new Compartment(); +const externalExtensionsCompartment = new Compartment(); +const highlightLinesCompartment = new Compartment(); + +const createHighlightLinesExtension = (range?: { start: number; end: number }): Extension => { + if (!range) { + return []; + } + + const start = Math.max(1, range.start); + const end = Math.max(start, range.end); + + return ViewPlugin.fromClass(class { + decorations; + + constructor(view: EditorView) { + this.decorations = this.build(view); + } + + update(update: import('@codemirror/view').ViewUpdate) { + if (update.docChanged || update.viewportChanged) { + this.decorations = this.build(update.view); + } + } + + build(view: EditorView) { + const builder = new RangeSetBuilder(); + for (let lineNo = start; lineNo <= end && lineNo <= view.state.doc.lines; lineNo += 1) { + const line = view.state.doc.line(lineNo); + builder.add(line.from, line.from, Decoration.line({ class: 'oc-cm-selected-line' })); + } + return builder.finish(); + } + }, { decorations: (v) => v.decorations }); +}; + +export function CodeMirrorEditor({ value, onChange, extensions, className, readOnly, lineNumbersConfig, highlightLines }: CodeMirrorEditorProps) { + const hostRef = React.useRef(null); + const viewRef = React.useRef(null); + const valueRef = React.useRef(value); + const onChangeRef = React.useRef(onChange); + + React.useEffect(() => { + valueRef.current = value; + }, [value]); + + React.useEffect(() => { + onChangeRef.current = onChange; + }, [onChange]); + + React.useEffect(() => { + if (!hostRef.current) { + return; + } + + const state = EditorState.create({ + doc: valueRef.current, + extensions: [ + gutters({ fixed: true }), + lineNumbersCompartment.of(lineNumbers(lineNumbersConfig)), + history(), + indentUnit.of(' '), + keymap.of([indentWithTab, ...defaultKeymap, ...historyKeymap]), + EditorView.updateListener.of((update) => { + if (!update.docChanged) { + return; + } + const next = update.state.doc.toString(); + valueRef.current = next; + onChangeRef.current(next); + }), + editableCompartment.of(EditorView.editable.of(!readOnly)), + externalExtensionsCompartment.of(extensions ?? []), + highlightLinesCompartment.of(createHighlightLinesExtension(highlightLines)), + ], + }); + + viewRef.current = new EditorView({ + state, + parent: hostRef.current, + }); + + return () => { + viewRef.current?.destroy(); + viewRef.current = null; + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + React.useEffect(() => { + const view = viewRef.current; + if (!view) { + return; + } + + view.dispatch({ + effects: [ + lineNumbersCompartment.reconfigure(lineNumbers(lineNumbersConfig)), + editableCompartment.reconfigure(EditorView.editable.of(!readOnly)), + externalExtensionsCompartment.reconfigure(extensions ?? []), + highlightLinesCompartment.reconfigure(createHighlightLinesExtension(highlightLines)), + ], + }); + }, [extensions, highlightLines, lineNumbersConfig, readOnly]); + + React.useEffect(() => { + const view = viewRef.current; + if (!view) { + return; + } + + const current = view.state.doc.toString(); + if (current !== value) { + view.dispatch({ + changes: { from: 0, to: current.length, insert: value }, + }); + } + }, [value]); + + return ( +
+ ); +} diff --git a/packages/ui/src/components/views/FilesView.tsx b/packages/ui/src/components/views/FilesView.tsx index ce2f4ec6..f5dff441 100644 --- a/packages/ui/src/components/views/FilesView.tsx +++ b/packages/ui/src/components/views/FilesView.tsx @@ -1,6 +1,5 @@ import React from 'react'; -import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; -import createElement from 'react-syntax-highlighter/create-element'; + import { RiArrowLeftSLine, RiClipboardLine, @@ -8,11 +7,13 @@ import { RiCodeLine, RiFileImageLine, RiFileTextLine, + RiFileCopy2Line, RiFolder3Fill, RiFolderOpenFill, RiLoader4Line, RiRefreshLine, RiSearchLine, + RiSave3Line, RiSendPlane2Line, RiTextWrap, } from '@remixicon/react'; @@ -22,14 +23,25 @@ import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay'; import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; import { Textarea } from '@/components/ui/textarea'; +import { CodeMirrorEditor } from '@/components/ui/CodeMirrorEditor'; +import { languageByExtension } from '@/lib/codemirror/languageByExtension'; +import { createFlexokiCodeMirrorTheme } from '@/lib/codemirror/flexokiTheme'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; import { useDebouncedValue } from '@/hooks/useDebouncedValue'; import { useFileSearchStore } from '@/stores/useFileSearchStore'; import { useDeviceInfo } from '@/lib/device'; -import { cn, getModifierLabel } from '@/lib/utils'; +import { cn, getModifierLabel, hasModifier } from '@/lib/utils'; import { getLanguageFromExtension, getImageMimeType, isImageFile } from '@/lib/toolHelpers'; import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs'; +import { EditorView } from '@codemirror/view'; import { useThemeSystem } from '@/contexts/useThemeSystem'; -import { generateSyntaxTheme } from '@/lib/theme/syntaxThemeGenerator'; import { useSessionStore } from '@/stores/useSessionStore'; import { useConfigStore } from '@/stores/useConfigStore'; import { useContextStore } from '@/stores/contextStore'; @@ -81,7 +93,6 @@ const useEffectiveDirectory = () => { return worktreeMetadata?.path ?? sessionDirectory ?? fallbackDirectory ?? ''; }; -const MAX_HIGHLIGHT_CHARS = 200_000; const MAX_VIEW_CHARS = 200_000; const CODE_EXTENSIONS = new Set([ @@ -221,8 +232,7 @@ const getFileIcon = (extension?: string): React.ReactNode => { export const FilesView: React.FC = () => { const { files, runtime } = useRuntimeAPIs(); const { currentTheme } = useThemeSystem(); - const syntaxTheme = React.useMemo(() => generateSyntaxTheme(currentTheme), [currentTheme]); - const { isMobile } = useDeviceInfo(); + const { isMobile, screenWidth } = useDeviceInfo(); const currentDirectory = useEffectiveDirectory(); const root = normalizePath(currentDirectory); @@ -249,6 +259,14 @@ export const FilesView: React.FC = () => { const [fileError, setFileError] = React.useState(null); const [desktopImageSrc, setDesktopImageSrc] = React.useState(''); + const [draftContent, setDraftContent] = React.useState(''); + const [isSaving, setIsSaving] = React.useState(false); + + const [confirmDiscardOpen, setConfirmDiscardOpen] = React.useState(false); + const pendingSelectFileRef = React.useRef(null); + const pendingTabRef = React.useRef(null); + const skipDirtyOnceRef = React.useRef(false); + // Line selection state for commenting const [lineSelection, setLineSelection] = React.useState(null); const [commentText, setCommentText] = React.useState(''); @@ -263,62 +281,15 @@ export const FilesView: React.FC = () => { const getAgentModelForSession = useContextStore((state) => state.getAgentModelForSession); const getAgentModelVariantForSession = useContextStore((state) => state.getAgentModelVariantForSession); const setActiveMainTab = useUIStore((state) => state.setActiveMainTab); + const setMainTabGuard = useUIStore((state) => state.setMainTabGuard); const { inputBarOffset, isKeyboardOpen } = useUIStore(); - // Line selection handlers - const handleLineClick = React.useCallback((lineNumber: number, shiftKey: boolean) => { - if (shiftKey && lineSelection) { - // Extend selection with shift+click - const newStart = Math.min(lineSelection.start, lineNumber); - const newEnd = Math.max(lineSelection.end, lineNumber); - setLineSelection({ start: newStart, end: newEnd }); - } else { - // Start new selection - setLineSelection({ start: lineNumber, end: lineNumber }); - } - }, [lineSelection]); - - const handleLineMouseDown = React.useCallback((lineNumber: number, e: React.MouseEvent) => { - e.preventDefault(); // Prevent text selection while selecting lines - if (e.shiftKey && lineSelection) { - // Shift+click extends selection - handleLineClick(lineNumber, true); - return; - } - isSelectingRef.current = true; - selectionStartRef.current = lineNumber; - setLineSelection({ start: lineNumber, end: lineNumber }); - }, [handleLineClick, lineSelection]); - - const handleLineMouseEnter = React.useCallback((lineNumber: number) => { - if (!isSelectingRef.current || selectionStartRef.current === null) return; - const start = Math.min(selectionStartRef.current, lineNumber); - const end = Math.max(selectionStartRef.current, lineNumber); - setLineSelection({ start, end }); - }, []); - - const handleLineMouseUp = React.useCallback(() => { - isSelectingRef.current = false; - }, []); - - // Mobile: tap to extend selection - const handleLineTap = React.useCallback((lineNumber: number) => { - if (lineSelection) { - // Extend selection to tapped line - const newStart = Math.min(lineSelection.start, lineSelection.end, lineNumber); - const newEnd = Math.max(lineSelection.start, lineSelection.end, lineNumber); - if (lineNumber < lineSelection.start || lineNumber > lineSelection.end) { - setLineSelection({ start: newStart, end: newEnd }); - return; - } - } - setLineSelection({ start: lineNumber, end: lineNumber }); - }, [lineSelection]); // Global mouseup to end drag selection React.useEffect(() => { const handleGlobalMouseUp = () => { isSelectingRef.current = false; + selectionStartRef.current = null; }; document.addEventListener('mouseup', handleGlobalMouseUp); return () => document.removeEventListener('mouseup', handleGlobalMouseUp); @@ -328,7 +299,10 @@ export const FilesView: React.FC = () => { React.useEffect(() => { setLineSelection(null); setCommentText(''); - }, [selectedFile?.path]); + setMainTabGuard(null); + setDraftContent(''); + setIsSaving(false); + }, [selectedFile?.path, setMainTabGuard]); // Click outside to dismiss selection React.useEffect(() => { @@ -341,8 +315,8 @@ export const FilesView: React.FC = () => { const commentUI = document.querySelector('[data-comment-ui]'); if (commentUI?.contains(target)) return; - // Check if click is on a line number (only line numbers should not dismiss) - if (target.closest('[data-line-number]')) return; + // Check if click is on CM gutter (only gutter should not dismiss) + if (target.closest('.cm-gutterElement')) return; // Check if click is inside toast (sonner) if (target.closest('[data-sonner-toast]') || target.closest('[data-sonner-toaster]')) return; @@ -652,7 +626,94 @@ export const FilesView: React.FC = () => { return response.text(); }, [files]); + const displayedContent = React.useMemo(() => { + return fileContent.length > MAX_VIEW_CHARS + ? `${fileContent.slice(0, MAX_VIEW_CHARS)}\n\n… truncated …` + : fileContent; + }, [fileContent]); + + const isDirty = React.useMemo(() => draftContent !== displayedContent, [draftContent, displayedContent]); + + const saveDraft = React.useCallback(async () => { + if (!selectedFile || !files.writeFile) { + toast.error('Saving not supported'); + return; + } + + if (!isDirty) { + return; + } + + setIsSaving(true); + + try { + const result = await files.writeFile(selectedFile.path, draftContent); + if (!result?.success) { + throw new Error('Failed to write file'); + } + setFileContent(draftContent); + } catch (error) { + toast.error(error instanceof Error ? error.message : 'Save failed'); + } finally { + setIsSaving(false); + } + }, [draftContent, files, isDirty, selectedFile]); + + React.useEffect(() => { + if (!isDirty) { + setMainTabGuard(null); + return; + } + + const guard = (_nextTab: import('@/stores/useUIStore').MainTab) => { + if (skipDirtyOnceRef.current) { + skipDirtyOnceRef.current = false; + return true; + } + setConfirmDiscardOpen(true); + pendingTabRef.current = _nextTab; + return false; + }; + + setMainTabGuard(guard); + + return () => { + const currentGuard = useUIStore.getState().mainTabGuard; + if (currentGuard === guard) { + setMainTabGuard(null); + } + }; + }, [isDirty, setMainTabGuard]); + + React.useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (!hasModifier(e)) { + return; + } + + if (e.key.toLowerCase() !== 's') { + return; + } + + e.preventDefault(); + if (!isSaving) { + void saveDraft(); + } + }; + + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [isSaving, saveDraft]); + const handleSelectFile = React.useCallback(async (node: FileNode) => { + if (skipDirtyOnceRef.current) { + skipDirtyOnceRef.current = false; + } else if (isDirty) { + setConfirmDiscardOpen(true); + pendingSelectFileRef.current = node; + return; + } + setSelectedFile(node); setFileError(null); setDesktopImageSrc(''); @@ -684,13 +745,68 @@ export const FilesView: React.FC = () => { try { const content = await readFile(node.path); setFileContent(content); + setDraftContent(content.length > MAX_VIEW_CHARS + ? `${content.slice(0, MAX_VIEW_CHARS)}\n\n… truncated …` + : content); } catch (error) { setFileContent(''); + setDraftContent(''); setFileError(error instanceof Error ? error.message : 'Failed to read file'); } finally { setFileLoading(false); } - }, [isMobile, readFile, runtime.isDesktop]); + }, [isDirty, isMobile, readFile, runtime.isDesktop]); + + const discardAndContinue = React.useCallback(() => { + const nextFile = pendingSelectFileRef.current; + const nextTab = pendingTabRef.current; + + pendingSelectFileRef.current = null; + pendingTabRef.current = null; + + // Allow one guarded navigation (tab/file) without re-opening dialog. + skipDirtyOnceRef.current = true; + + setConfirmDiscardOpen(false); + + // Discard draft by reverting back to last loaded content + setDraftContent(displayedContent); + + if (nextFile) { + void handleSelectFile(nextFile); + return; + } + + if (nextTab) { + setMainTabGuard(null); + useUIStore.getState().setActiveMainTab(nextTab); + } + }, [displayedContent, handleSelectFile, setMainTabGuard]); + + const saveAndContinue = React.useCallback(async () => { + const nextFile = pendingSelectFileRef.current; + const nextTab = pendingTabRef.current; + + pendingSelectFileRef.current = null; + pendingTabRef.current = null; + + // We'll proceed after saving; suppress guard reopening. + skipDirtyOnceRef.current = true; + + setConfirmDiscardOpen(false); + + await saveDraft(); + + if (nextFile) { + await handleSelectFile(nextFile); + return; + } + + if (nextTab) { + setMainTabGuard(null); + useUIStore.getState().setActiveMainTab(nextTab); + } + }, [handleSelectFile, saveDraft, setMainTabGuard]); const toggleDirectory = React.useCallback(async (dirPath: string) => { const normalized = normalizePath(dirPath); @@ -763,11 +879,6 @@ export const FilesView: React.FC = () => { }); }, [childrenByDir, expandedDirs, handleSelectFile, selectedFile?.path, toggleDirectory]); - const viewerLanguage = selectedFile?.path ? getLanguageFromExtension(selectedFile.path) || 'text' : 'text'; - const contentForViewer = fileContent.length > MAX_VIEW_CHARS - ? `${fileContent.slice(0, MAX_VIEW_CHARS)}\n\n… truncated …` - : fileContent; - const shouldHighlight = contentForViewer.length <= MAX_HIGHLIGHT_CHARS; const isSelectedImage = Boolean(selectedFile?.path && isImageFile(selectedFile.path)); const isSelectedSvg = Boolean(selectedFile?.path && selectedFile.path.toLowerCase().endsWith('.svg')); const displaySelectedPath = React.useMemo(() => { @@ -783,6 +894,24 @@ export const FilesView: React.FC = () => { const canCopy = Boolean(selectedFile && (!isSelectedImage || isSelectedSvg) && fileContent.length > 0); + const canCopyPath = Boolean(selectedFile && displaySelectedPath.length > 0); + const canEdit = Boolean(selectedFile && !isSelectedImage && files.writeFile && fileContent.length <= MAX_VIEW_CHARS); + + const editorExtensions = React.useMemo(() => { + if (!selectedFile?.path) { + return [createFlexokiCodeMirrorTheme(currentTheme)]; + } + + const extensions = [createFlexokiCodeMirrorTheme(currentTheme)]; + const language = languageByExtension(selectedFile.path); + if (language) { + extensions.push(language); + } + if (wrapLines) { + extensions.push(EditorView.lineWrapping); + } + return extensions; + }, [currentTheme, selectedFile?.path, wrapLines]); const imageSrc = selectedFile?.path && isSelectedImage ? (runtime.isDesktop @@ -794,83 +923,7 @@ export const FilesView: React.FC = () => { : `/api/fs/raw?path=${encodeURIComponent(selectedFile.path)}`)) : ''; - const codeRenderer = React.useCallback(({ - rows, - stylesheet, - useInlineStyles, - }: { - rows: unknown[]; - stylesheet: unknown; - useInlineStyles: boolean; - }) => { - const gutterWidthCh = Math.max(3, String(rows.length).length + 1); - return ( -
- {rows.map((row, index) => { - const lineNumber = index + 1; - const isSelected = lineSelection !== null && lineNumber >= lineSelection.start && lineNumber <= lineSelection.end; - - return ( -
handleLineMouseEnter(lineNumber)} - style={{ - display: 'flex', - alignItems: 'flex-start', - lineHeight: '1.5rem', - position: 'relative', - backgroundColor: isSelected ? 'color-mix(in srgb, var(--accent) 70%, transparent)' : undefined, - }} - > - handleLineMouseDown(lineNumber, e)} - onMouseUp={isMobile ? undefined : handleLineMouseUp} - onClick={isMobile ? () => handleLineTap(lineNumber) : undefined} - style={{ - width: `calc(${gutterWidthCh}ch + 0.75rem + 0.75rem)`, - flexShrink: 0, - paddingLeft: '0.75rem', - paddingRight: '1.75ch', - textAlign: 'right', - color: 'hsl(var(--muted-foreground))', - opacity: 0.35, - fontSize: '0.8em', - lineHeight: '1.5rem', - userSelect: 'none', - WebkitUserSelect: 'none', - MozUserSelect: 'none', - msUserSelect: 'none' as const, - cursor: 'pointer', - touchAction: 'manipulation', - }} - > - {lineNumber} - - - {createElement({ node: row, stylesheet, useInlineStyles, key: index })} - -
- ); - })} -
- ); - }, [wrapLines, lineSelection, isMobile, handleLineMouseDown, handleLineMouseEnter, handleLineMouseUp, handleLineTap]); React.useEffect(() => { @@ -999,9 +1052,35 @@ export const FilesView: React.FC = () => { const fileViewer = (
-
+ { + // Intentionally no "cancel" action. Keep dialog modal. + if (!open) { + setConfirmDiscardOpen(true); + } + }}> + + + Unsaved changes + + Save your edits before continuing? + + + + + + + + +
{isMobile && showMobilePageContent && (
-
+
+ {canEdit && ( + + )} + + {canEdit && selectedFile && !isSelectedImage && ( +
-
- +
+ {!selectedFile ? (
Pick a file from the tree.
) : fileLoading ? ( @@ -1081,32 +1207,79 @@ export const FilesView: React.FC = () => { />
) : ( -
- + { + if (!(event instanceof MouseEvent)) { + return false; + } + if (event.button !== 0) { + return false; + } + event.preventDefault(); + + const lineNumber = view.state.doc.lineAt(line.from).number; + + // Mobile: tap-to-extend selection + if (isMobile && lineSelection && !event.shiftKey) { + const start = Math.min(lineSelection.start, lineSelection.end, lineNumber); + const end = Math.max(lineSelection.start, lineSelection.end, lineNumber); + setLineSelection({ start, end }); + isSelectingRef.current = false; + selectionStartRef.current = null; + return true; + } + + isSelectingRef.current = true; + selectionStartRef.current = lineNumber; + + if (lineSelection && event.shiftKey) { + const start = Math.min(lineSelection.start, lineNumber); + const end = Math.max(lineSelection.end, lineNumber); + setLineSelection({ start, end }); + } else { + setLineSelection({ start: lineNumber, end: lineNumber }); + } + + return true; + }, + mouseover: (view: EditorView, line: { from: number; to: number }, event: Event) => { + if (!(event instanceof MouseEvent)) { + return false; + } + if (event.buttons !== 1) { + return false; + } + if (!isSelectingRef.current || selectionStartRef.current === null) { + return false; + } + + const lineNumber = view.state.doc.lineAt(line.from).number; + const start = Math.min(selectionStartRef.current, lineNumber); + const end = Math.max(selectionStartRef.current, lineNumber); + setLineSelection({ start, end }); + return false; + }, + mouseup: () => { + isSelectingRef.current = false; + selectionStartRef.current = null; + return false; + }, }, }} - > - {contentForViewer} - + />
)}
@@ -1225,16 +1398,18 @@ export const FilesView: React.FC = () => { ) : ( treePanel ) - ) : ( -
-
- {treePanel} -
-
- {fileViewer} -
-
- )} + ) : ( +
+ {screenWidth >= 1024 && ( +
+ {treePanel} +
+ )} +
+ {fileViewer} +
+
+ )}
); }; diff --git a/packages/ui/src/index.css b/packages/ui/src/index.css index 13be3d63..f30f0183 100644 --- a/packages/ui/src/index.css +++ b/packages/ui/src/index.css @@ -46,6 +46,10 @@ textarea[data-chat-input="true"]:hover { box-shadow: none; } +.oc-cm-selected-line { + background: color-mix(in srgb, var(--accent) 70%, transparent); +} + :root.vscode-runtime textarea[data-chat-input="true"]::placeholder { color: color-mix(in srgb, var(--vscode-input-placeholderForeground, var(--muted-foreground)) 65%, transparent); } diff --git a/packages/ui/src/lib/codemirror/flexokiTheme.ts b/packages/ui/src/lib/codemirror/flexokiTheme.ts new file mode 100644 index 00000000..6d5d9ebf --- /dev/null +++ b/packages/ui/src/lib/codemirror/flexokiTheme.ts @@ -0,0 +1,193 @@ +import type { Extension } from '@codemirror/state'; + +import { EditorView } from '@codemirror/view'; +import { HighlightStyle, syntaxHighlighting } from '@codemirror/language'; +import { tags as t } from '@lezer/highlight'; + +import type { Theme } from '@/types/theme'; + +export function createFlexokiCodeMirrorTheme(theme: Theme): Extension { + const isDark = theme.metadata.variant === 'dark'; + + const monoFont = theme.config?.fonts?.mono || 'monospace'; + const highlights = theme.colors.syntax.highlights || {}; + const tokens = theme.colors.syntax.tokens || {}; + + const ui = EditorView.theme({ + '&': { + backgroundColor: 'var(--background)', + color: theme.colors.syntax.base.foreground, + fontSize: 'var(--text-code)', + lineHeight: '1.5rem', + }, + '.cm-scroller': { + fontFamily: monoFont, + backgroundColor: 'var(--background)', + }, + + /* StreamLanguage/legacy-modes tokens (class-based) */ + '.cm-comment': { + color: theme.colors.syntax.base.comment, + }, + '.cm-keyword': { + color: theme.colors.syntax.base.keyword, + }, + '.cm-string': { + color: theme.colors.syntax.base.string, + }, + '.cm-string-2': { + color: tokens.stringEscape || theme.colors.syntax.base.string, + }, + '.cm-number': { + color: theme.colors.syntax.base.number, + }, + '.cm-atom': { + color: tokens.boolean || theme.colors.syntax.base.number, + }, + '.cm-builtin': { + color: tokens.functionCall || theme.colors.syntax.base.function, + }, + '.cm-def': { + color: tokens.variableGlobal || theme.colors.syntax.base.variable, + }, + // Legacy shell flags (--foo, -bar) + '.cm-attribute': { + color: tokens.variableOther || tokens.variableProperty || theme.colors.syntax.base.operator, + }, + '.cm-meta': { + color: theme.colors.syntax.base.comment, + }, + '.cm-property': { + color: tokens.variableProperty || theme.colors.syntax.base.keyword, + }, + '.cm-variable': { + color: theme.colors.syntax.base.variable, + }, + '.cm-variable-2': { + color: tokens.variableOther || theme.colors.syntax.base.function, + }, + '.cm-variable-3': { + color: tokens.variableGlobal || theme.colors.syntax.base.type, + }, + '.cm-tag': { + color: tokens.tag || theme.colors.syntax.base.keyword, + }, + '.cm-link': { + color: tokens.url || theme.colors.syntax.base.keyword, + textDecoration: 'underline', + }, + '.cm-content': { + caretColor: theme.colors.interactive.cursor, + }, + '.cm-cursor, .cm-dropCursor': { + borderLeftColor: theme.colors.interactive.cursor, + }, + '&.cm-focused .cm-selectionBackground, .cm-selectionBackground, ::selection': { + backgroundColor: theme.colors.interactive.selection, + }, + '.cm-gutters': { + backgroundColor: 'var(--background)', + color: highlights.lineNumber || theme.colors.syntax.base.comment, + borderRight: `1px solid ${theme.colors.interactive.border}`, + position: 'sticky', + paddingRight: '8px', + left: 0, + zIndex: 2, + boxShadow: `0 0 0 var(--background)`, + }, + '.cm-gutter': { + backgroundColor: 'var(--background)', + }, + '.cm-gutterElement': { + backgroundColor: 'var(--background)', + }, + '.cm-lineNumbers': { + backgroundColor: 'var(--background)', + }, + '.cm-lineNumbers .cm-gutterElement': { + paddingLeft: '8px', + paddingRight: '8px', + minWidth: '42px', + }, + '.cm-activeLineGutter': { + color: highlights.lineNumberActive || theme.colors.syntax.base.foreground, + }, + '.cm-activeLine': { + backgroundColor: theme.colors.surface.overlay, + }, + '&.cm-focused': { + outline: 'none', + }, + }, { dark: isDark }); + + const syntax = HighlightStyle.define([ + { tag: t.comment, color: theme.colors.syntax.base.comment }, + { tag: t.docComment, color: tokens.commentDoc || theme.colors.syntax.base.comment }, + + { tag: t.keyword, color: theme.colors.syntax.base.keyword }, + { tag: t.controlKeyword, color: theme.colors.syntax.base.keyword }, + { tag: t.operatorKeyword, color: theme.colors.syntax.base.operator }, + { tag: t.moduleKeyword, color: tokens.keywordImport || theme.colors.syntax.base.keyword }, + { tag: [t.definitionKeyword, t.modifier], color: tokens.storageModifier || theme.colors.syntax.base.keyword }, + + { tag: t.atom, color: tokens.boolean || theme.colors.syntax.base.number }, + { tag: [t.null, t.self], color: theme.colors.syntax.base.number }, + { tag: [t.meta, t.documentMeta], color: theme.colors.syntax.base.comment }, + + { tag: t.string, color: theme.colors.syntax.base.string }, + { tag: t.escape, color: tokens.stringEscape || theme.colors.syntax.base.foreground }, + { tag: t.regexp, color: tokens.regex || theme.colors.syntax.base.string }, + + { tag: t.number, color: theme.colors.syntax.base.number }, + { tag: t.bool, color: tokens.boolean || theme.colors.syntax.base.number }, + + // Operators + punctuation + { tag: t.operator, color: theme.colors.syntax.base.operator }, + { tag: [t.derefOperator, t.updateOperator, t.definitionOperator, t.typeOperator, t.controlOperator], color: theme.colors.syntax.base.operator }, + { tag: [t.logicOperator, t.bitwiseOperator, t.arithmeticOperator], color: theme.colors.syntax.base.operator }, + { tag: [t.compareOperator], color: tokens.diffModified || theme.colors.syntax.base.operator }, + { tag: [t.punctuation, t.separator, t.bracket, t.paren, t.brace, t.squareBracket, t.angleBracket], color: tokens.punctuation || theme.colors.syntax.base.comment }, + + // Calls vs definitions + { tag: t.function(t.variableName), color: tokens.functionCall || theme.colors.syntax.base.function }, + { tag: t.function(t.definition(t.variableName)), color: theme.colors.syntax.base.function }, + { tag: t.function(t.propertyName), color: tokens.method || tokens.functionCall || theme.colors.syntax.base.function }, + + // Names + { tag: t.namespace, color: tokens.namespace || theme.colors.syntax.base.type }, + { tag: t.moduleKeyword, color: tokens.module || theme.colors.syntax.base.keyword }, + { tag: t.macroName, color: tokens.macro || theme.colors.syntax.base.keyword }, + { tag: t.labelName, color: tokens.label || theme.colors.syntax.base.keyword }, + { tag: t.annotation, color: tokens.decorator || theme.colors.syntax.base.keyword }, + + // Variables/properties + { tag: t.propertyName, color: tokens.variableProperty || theme.colors.syntax.base.keyword }, + { tag: t.attributeName, color: tokens.tagAttribute || theme.colors.syntax.base.keyword }, + + // StreamLanguage/legacy token tags resolve to these + { tag: t.standard(t.variableName), color: tokens.method || theme.colors.syntax.base.function }, + { tag: t.definition(t.variableName), color: theme.colors.syntax.base.variable }, + { tag: t.local(t.variableName), color: theme.colors.syntax.base.variable }, + { tag: t.special(t.variableName), color: tokens.variableOther || theme.colors.syntax.base.function }, + { tag: t.variableName, color: theme.colors.syntax.base.variable }, + { tag: t.special(t.string), color: theme.colors.syntax.base.string }, + + // Types/constants + { tag: t.className, color: tokens.className || theme.colors.syntax.base.type }, + { tag: t.typeName, color: theme.colors.syntax.base.type }, + { tag: t.constant(t.variableName), color: tokens.constant || theme.colors.syntax.base.variable }, + { tag: t.literal, color: tokens.constant || theme.colors.syntax.base.variable }, + + // Markup + { tag: t.tagName, color: tokens.tag || theme.colors.syntax.base.keyword }, + { tag: t.attributeValue, color: tokens.tagAttributeValue || theme.colors.syntax.base.string }, + + // Markdown-ish + { tag: [t.heading, t.heading1, t.heading2, t.heading3, t.heading4, t.heading5, t.heading6], color: theme.colors.syntax.base.keyword, fontWeight: '600' }, + { tag: t.monospace, color: theme.colors.syntax.base.string }, + + { tag: t.link, color: tokens.url || theme.colors.syntax.base.keyword, textDecoration: 'underline' }, + ]); + + return [ui, syntaxHighlighting(syntax)]; +} diff --git a/packages/ui/src/lib/codemirror/languageByExtension.ts b/packages/ui/src/lib/codemirror/languageByExtension.ts new file mode 100644 index 00000000..2bd99e83 --- /dev/null +++ b/packages/ui/src/lib/codemirror/languageByExtension.ts @@ -0,0 +1,191 @@ +import type { Extension } from '@codemirror/state'; + +import { javascript } from '@codemirror/lang-javascript'; +import { json } from '@codemirror/lang-json'; +import { css } from '@codemirror/lang-css'; +import { html } from '@codemirror/lang-html'; +import { markdown } from '@codemirror/lang-markdown'; +import { languages } from '@codemirror/language-data'; +import { python } from '@codemirror/lang-python'; +import { sql } from '@codemirror/lang-sql'; +import { xml } from '@codemirror/lang-xml'; +import { yaml as yamlLanguage } from '@codemirror/lang-yaml'; +import { rust } from '@codemirror/lang-rust'; + +import { Language, LanguageDescription, StreamLanguage } from '@codemirror/language'; +import { shell } from '@codemirror/legacy-modes/mode/shell'; +import { toml } from '@codemirror/legacy-modes/mode/toml'; +import { diff } from '@codemirror/legacy-modes/mode/diff'; +import { dockerFile } from '@codemirror/legacy-modes/mode/dockerfile'; +import { ruby } from '@codemirror/legacy-modes/mode/ruby'; +import { properties } from '@codemirror/legacy-modes/mode/properties'; + +const shellLanguage = StreamLanguage.define(shell); +const tomlLanguage = StreamLanguage.define(toml); +const diffLanguage = StreamLanguage.define(diff); +const dockerfileLanguage = StreamLanguage.define(dockerFile); +const rubyLanguage = StreamLanguage.define(ruby); +const propertiesLanguage = StreamLanguage.define(properties); + +function codeBlockLanguageResolver(info: string): Language | LanguageDescription | null { + const normalized = info.trim().toLowerCase(); + + switch (normalized) { + case 'bash': + case 'sh': + case 'zsh': + case 'shell': + case 'shellsession': + case 'console': + return shellLanguage; + case 'toml': + return tomlLanguage; + case 'diff': + case 'patch': + return diffLanguage; + case 'json': + case 'jsonc': + case 'json5': + return json().language; + case 'js': + case 'javascript': + return javascript().language; + case 'jsx': + return javascript({ jsx: true }).language; + case 'ts': + case 'typescript': + return javascript({ typescript: true }).language; + case 'tsx': + return javascript({ typescript: true, jsx: true }).language; + case 'yaml': + case 'yml': + return yamlLanguage().language; + case 'html': + return html().language; + case 'css': + return css().language; + case 'xml': + case 'svg': + return xml().language; + case 'py': + case 'python': + return python().language; + case 'sql': + return sql().language; + case 'rs': + case 'rust': + return rust().language; + default: + return LanguageDescription.matchLanguageName(languages, normalized, true); + } +} + +const normalizeFileName = (filePath: string) => filePath.split('/').pop()?.toLowerCase() ?? ''; + +export function languageByExtension(filePath: string): Extension | null { + const normalized = filePath.toLowerCase(); + const filename = normalizeFileName(normalized); + + // Special filenames + switch (filename) { + case 'dockerfile': + return dockerfileLanguage; + case 'makefile': + case 'gnumakefile': + // No dedicated mode; shell is a decent fallback for Make-ish files. + return shellLanguage; + } + + const idx = normalized.lastIndexOf('.'); + const ext = idx >= 0 ? normalized.slice(idx + 1) : ''; + + switch (ext) { + // JavaScript/TypeScript + case 'ts': + case 'tsx': + case 'mts': + case 'cts': + return javascript({ typescript: true, jsx: ext === 'tsx' }); + case 'js': + case 'jsx': + case 'mjs': + case 'cjs': + return javascript({ typescript: false, jsx: ext === 'jsx' }); + + // Web + case 'json': + case 'jsonc': + case 'json5': + case 'jsonl': + case 'ndjson': + case 'geojson': + return json(); + case 'css': + case 'scss': + case 'sass': + case 'less': + return css(); + case 'html': + case 'htm': + return html(); + case 'md': + case 'mdx': + case 'markdown': + case 'mdown': + case 'mkd': + return markdown({ + codeLanguages: codeBlockLanguageResolver, + }); + + // Data/config + case 'yml': + case 'yaml': + return yamlLanguage(); + case 'toml': + return tomlLanguage; + case 'ini': + case 'cfg': + case 'conf': + case 'config': + case 'properties': + return propertiesLanguage; + + // Shell + case 'sh': + case 'bash': + case 'zsh': + case 'fish': + case 'env': + return shellLanguage; + + // Languages we already ship + case 'py': + case 'pyw': + case 'pyi': + return python(); + case 'sql': + case 'psql': + case 'plsql': + return sql(); + case 'xml': + case 'xsl': + case 'xslt': + case 'xsd': + case 'dtd': + case 'plist': + case 'svg': + return xml(); + case 'rs': + return rust(); + + // Legacy modes + case 'rb': + case 'erb': + case 'rake': + case 'gemspec': + return rubyLanguage; + + default: + return null; + } +} diff --git a/packages/ui/src/stores/useUIStore.ts b/packages/ui/src/stores/useUIStore.ts index 2556ce72..8ca178f2 100644 --- a/packages/ui/src/stores/useUIStore.ts +++ b/packages/ui/src/stores/useUIStore.ts @@ -5,6 +5,8 @@ import { getSafeStorage } from './utils/safeStorage'; import { SEMANTIC_TYPOGRAPHY, getTypographyVariable, type SemanticTypographyKey } from '@/lib/typography'; export type MainTab = 'chat' | 'git' | 'diff' | 'terminal' | 'files'; + +export type MainTabGuard = (nextTab: MainTab) => boolean; export type EventStreamStatus = | 'idle' | 'connecting' @@ -24,6 +26,7 @@ interface UIStore { hasManuallyResizedLeftSidebar: boolean; isSessionSwitcherOpen: boolean; activeMainTab: MainTab; + mainTabGuard: MainTabGuard | null; pendingDiffFile: string | null; isMobile: boolean; isKeyboardOpen: boolean; @@ -64,6 +67,7 @@ interface UIStore { setSidebarWidth: (width: number) => void; setSessionSwitcherOpen: (open: boolean) => void; setActiveMainTab: (tab: MainTab) => void; + setMainTabGuard: (guard: MainTabGuard | null) => void; setPendingDiffFile: (filePath: string | null) => void; navigateToDiff: (filePath: string) => void; consumePendingDiffFile: () => string | null; @@ -120,6 +124,7 @@ export const useUIStore = create()( hasManuallyResizedLeftSidebar: false, isSessionSwitcherOpen: false, activeMainTab: 'chat', + mainTabGuard: null, pendingDiffFile: null, isMobile: false, isKeyboardOpen: false, @@ -195,7 +200,15 @@ export const useUIStore = create()( set({ isSessionSwitcherOpen: open }); }, + setMainTabGuard: (guard) => { + set({ mainTabGuard: guard }); + }, + setActiveMainTab: (tab) => { + const guard = get().mainTabGuard; + if (guard && !guard(tab)) { + return; + } set({ activeMainTab: tab }); }, @@ -204,6 +217,10 @@ export const useUIStore = create()( }, navigateToDiff: (filePath) => { + const guard = get().mainTabGuard; + if (guard && !guard('diff')) { + return; + } set({ pendingDiffFile: filePath, activeMainTab: 'diff' }); }, diff --git a/packages/web/src/api/files.ts b/packages/web/src/api/files.ts index ba740b45..d5c82261 100644 --- a/packages/web/src/api/files.ts +++ b/packages/web/src/api/files.ts @@ -138,4 +138,24 @@ export const createWebFilesAPI = (): FilesAPI => ({ const content = await response.text(); return { content, path: target }; }, + + async writeFile(path: string, content: string): Promise<{ success: boolean; path: string }> { + const target = normalizePath(path); + const response = await fetch('/api/fs/write', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ path: target, content }), + }); + + if (!response.ok) { + const error = await response.json().catch(() => ({ error: response.statusText })); + throw new Error((error as { error?: string }).error || 'Failed to write file'); + } + + const result = await response.json().catch(() => ({})); + return { + success: Boolean((result as { success?: boolean }).success), + path: typeof (result as { path?: string }).path === 'string' ? normalizePath((result as { path: string }).path) : target, + }; + }, });