feat: add built-in file editor with syntax highlighting
Security Scan and Upload / Security & DefectDojo Upload (pull_request) Successful in 58s
CI / Lint & Test (pull_request) Successful in 16m9s
CI / Build Linux (pull_request) Successful in 21m21s
CI / Build Windows (cross-compile) (pull_request) Successful in 34m43s

- Add CodeMirror 6 editor with support for 40+ languages
- Add file browser sidebar with directory tree navigation
- Add multi-tab support with dirty state indicators
- Add keyboard shortcuts (Ctrl+E, Ctrl+B, Ctrl+S, Ctrl+W)
- Add editor toggle button to status bar
- Editor uses current session's CWD and requires connection
- Add Tauri commands for file operations (list, read, write)
This commit is contained in:
2026-01-28 16:12:51 -08:00
committed by Naomi Carrigan
parent edc863e020
commit 392243f54f
14 changed files with 2015 additions and 3 deletions
+26
View File
@@ -27,6 +27,31 @@
}, },
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@codemirror/lang-angular": "^0.1.4",
"@codemirror/lang-cpp": "^6.0.3",
"@codemirror/lang-css": "^6.3.1",
"@codemirror/lang-go": "^6.0.1",
"@codemirror/lang-html": "^6.4.11",
"@codemirror/lang-java": "^6.0.2",
"@codemirror/lang-javascript": "^6.2.4",
"@codemirror/lang-json": "^6.0.2",
"@codemirror/lang-less": "^6.0.2",
"@codemirror/lang-markdown": "^6.5.0",
"@codemirror/lang-php": "^6.0.2",
"@codemirror/lang-python": "^6.2.1",
"@codemirror/lang-rust": "^6.0.2",
"@codemirror/lang-sass": "^6.0.2",
"@codemirror/lang-sql": "^6.10.0",
"@codemirror/lang-vue": "^0.1.3",
"@codemirror/lang-wast": "^6.0.2",
"@codemirror/lang-xml": "^6.1.0",
"@codemirror/lang-yaml": "^6.1.2",
"@codemirror/language": "^6.12.1",
"@codemirror/legacy-modes": "^6.5.2",
"@codemirror/state": "^6.5.4",
"@codemirror/theme-one-dark": "^6.1.3",
"@codemirror/view": "^6.39.11",
"@lezer/highlight": "^1.2.3",
"@tauri-apps/api": "^2", "@tauri-apps/api": "^2",
"@tauri-apps/plugin-clipboard-manager": "^2.3.2", "@tauri-apps/plugin-clipboard-manager": "^2.3.2",
"@tauri-apps/plugin-dialog": "^2", "@tauri-apps/plugin-dialog": "^2",
@@ -36,6 +61,7 @@
"@tauri-apps/plugin-os": "^2", "@tauri-apps/plugin-os": "^2",
"@tauri-apps/plugin-shell": "^2.3.4", "@tauri-apps/plugin-shell": "^2.3.4",
"@tauri-apps/plugin-store": "^2", "@tauri-apps/plugin-store": "^2",
"codemirror": "^6.0.2",
"highlight.js": "^11.11.1", "highlight.js": "^11.11.1",
"marked": "^17.0.1" "marked": "^17.0.1"
}, },
+549
View File
@@ -8,6 +8,81 @@ importers:
.: .:
dependencies: dependencies:
'@codemirror/lang-angular':
specifier: ^0.1.4
version: 0.1.4
'@codemirror/lang-cpp':
specifier: ^6.0.3
version: 6.0.3
'@codemirror/lang-css':
specifier: ^6.3.1
version: 6.3.1
'@codemirror/lang-go':
specifier: ^6.0.1
version: 6.0.1
'@codemirror/lang-html':
specifier: ^6.4.11
version: 6.4.11
'@codemirror/lang-java':
specifier: ^6.0.2
version: 6.0.2
'@codemirror/lang-javascript':
specifier: ^6.2.4
version: 6.2.4
'@codemirror/lang-json':
specifier: ^6.0.2
version: 6.0.2
'@codemirror/lang-less':
specifier: ^6.0.2
version: 6.0.2
'@codemirror/lang-markdown':
specifier: ^6.5.0
version: 6.5.0
'@codemirror/lang-php':
specifier: ^6.0.2
version: 6.0.2
'@codemirror/lang-python':
specifier: ^6.2.1
version: 6.2.1
'@codemirror/lang-rust':
specifier: ^6.0.2
version: 6.0.2
'@codemirror/lang-sass':
specifier: ^6.0.2
version: 6.0.2
'@codemirror/lang-sql':
specifier: ^6.10.0
version: 6.10.0
'@codemirror/lang-vue':
specifier: ^0.1.3
version: 0.1.3
'@codemirror/lang-wast':
specifier: ^6.0.2
version: 6.0.2
'@codemirror/lang-xml':
specifier: ^6.1.0
version: 6.1.0
'@codemirror/lang-yaml':
specifier: ^6.1.2
version: 6.1.2
'@codemirror/language':
specifier: ^6.12.1
version: 6.12.1
'@codemirror/legacy-modes':
specifier: ^6.5.2
version: 6.5.2
'@codemirror/state':
specifier: ^6.5.4
version: 6.5.4
'@codemirror/theme-one-dark':
specifier: ^6.1.3
version: 6.1.3
'@codemirror/view':
specifier: ^6.39.11
version: 6.39.11
'@lezer/highlight':
specifier: ^1.2.3
version: 1.2.3
'@tauri-apps/api': '@tauri-apps/api':
specifier: ^2 specifier: ^2
version: 2.9.1 version: 2.9.1
@@ -35,6 +110,9 @@ importers:
'@tauri-apps/plugin-store': '@tauri-apps/plugin-store':
specifier: ^2 specifier: ^2
version: 2.4.2 version: 2.4.2
codemirror:
specifier: ^6.0.2
version: 6.0.2
highlight.js: highlight.js:
specifier: ^11.11.1 specifier: ^11.11.1
version: 11.11.1 version: 11.11.1
@@ -158,6 +236,90 @@ packages:
resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==}
engines: {node: '>=18'} engines: {node: '>=18'}
'@codemirror/autocomplete@6.20.0':
resolution: {integrity: sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg==}
'@codemirror/commands@6.10.1':
resolution: {integrity: sha512-uWDWFypNdQmz2y1LaNJzK7fL7TYKLeUAU0npEC685OKTF3KcQ2Vu3klIM78D7I6wGhktme0lh3CuQLv0ZCrD9Q==}
'@codemirror/lang-angular@0.1.4':
resolution: {integrity: sha512-oap+gsltb/fzdlTQWD6BFF4bSLKcDnlxDsLdePiJpCVNKWXSTAbiiQeYI3UmES+BLAdkmIC1WjyztC1pi/bX4g==}
'@codemirror/lang-cpp@6.0.3':
resolution: {integrity: sha512-URM26M3vunFFn9/sm6rzqrBzDgfWuDixp85uTY49wKudToc2jTHUrKIGGKs+QWND+YLofNNZpxcNGRynFJfvgA==}
'@codemirror/lang-css@6.3.1':
resolution: {integrity: sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==}
'@codemirror/lang-go@6.0.1':
resolution: {integrity: sha512-7fNvbyNylvqCphW9HD6WFnRpcDjr+KXX/FgqXy5H5ZS0eC5edDljukm/yNgYkwTsgp2busdod50AOTIy6Jikfg==}
'@codemirror/lang-html@6.4.11':
resolution: {integrity: sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==}
'@codemirror/lang-java@6.0.2':
resolution: {integrity: sha512-m5Nt1mQ/cznJY7tMfQTJchmrjdjQ71IDs+55d1GAa8DGaB8JXWsVCkVT284C3RTASaY43YknrK2X3hPO/J3MOQ==}
'@codemirror/lang-javascript@6.2.4':
resolution: {integrity: sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA==}
'@codemirror/lang-json@6.0.2':
resolution: {integrity: sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==}
'@codemirror/lang-less@6.0.2':
resolution: {integrity: sha512-EYdQTG22V+KUUk8Qq582g7FMnCZeEHsyuOJisHRft/mQ+ZSZ2w51NupvDUHiqtsOy7It5cHLPGfHQLpMh9bqpQ==}
'@codemirror/lang-markdown@6.5.0':
resolution: {integrity: sha512-0K40bZ35jpHya6FriukbgaleaqzBLZfOh7HuzqbMxBXkbYMJDxfF39c23xOgxFezR+3G+tR2/Mup+Xk865OMvw==}
'@codemirror/lang-php@6.0.2':
resolution: {integrity: sha512-ZKy2v1n8Fc8oEXj0Th0PUMXzQJ0AIR6TaZU+PbDHExFwdu+guzOA4jmCHS1Nz4vbFezwD7LyBdDnddSJeScMCA==}
'@codemirror/lang-python@6.2.1':
resolution: {integrity: sha512-IRjC8RUBhn9mGR9ywecNhB51yePWCGgvHfY1lWN/Mrp3cKuHr0isDKia+9HnvhiWNnMpbGhWrkhuWOc09exRyw==}
'@codemirror/lang-rust@6.0.2':
resolution: {integrity: sha512-EZaGjCUegtiU7kSMvOfEZpaCReowEf3yNidYu7+vfuGTm9ow4mthAparY5hisJqOHmJowVH3Upu+eJlUji6qqA==}
'@codemirror/lang-sass@6.0.2':
resolution: {integrity: sha512-l/bdzIABvnTo1nzdY6U+kPAC51czYQcOErfzQ9zSm9D8GmNPD0WTW8st/CJwBTPLO8jlrbyvlSEcN20dc4iL0Q==}
'@codemirror/lang-sql@6.10.0':
resolution: {integrity: sha512-6ayPkEd/yRw0XKBx5uAiToSgGECo/GY2NoJIHXIIQh1EVwLuKoU8BP/qK0qH5NLXAbtJRLuT73hx7P9X34iO4w==}
'@codemirror/lang-vue@0.1.3':
resolution: {integrity: sha512-QSKdtYTDRhEHCfo5zOShzxCmqKJvgGrZwDQSdbvCRJ5pRLWBS7pD/8e/tH44aVQT6FKm0t6RVNoSUWHOI5vNug==}
'@codemirror/lang-wast@6.0.2':
resolution: {integrity: sha512-Imi2KTpVGm7TKuUkqyJ5NRmeFWF7aMpNiwHnLQe0x9kmrxElndyH0K6H/gXtWwY6UshMRAhpENsgfpSwsgmC6Q==}
'@codemirror/lang-xml@6.1.0':
resolution: {integrity: sha512-3z0blhicHLfwi2UgkZYRPioSgVTo9PV5GP5ducFH6FaHy0IAJRg+ixj5gTR1gnT/glAIC8xv4w2VL1LoZfs+Jg==}
'@codemirror/lang-yaml@6.1.2':
resolution: {integrity: sha512-dxrfG8w5Ce/QbT7YID7mWZFKhdhsaTNOYjOkSIMt1qmC4VQnXSDSYVHHHn8k6kJUfIhtLo8t1JJgltlxWdsITw==}
'@codemirror/language@6.12.1':
resolution: {integrity: sha512-Fa6xkSiuGKc8XC8Cn96T+TQHYj4ZZ7RdFmXA3i9xe/3hLHfwPZdM+dqfX0Cp0zQklBKhVD8Yzc8LS45rkqcwpQ==}
'@codemirror/legacy-modes@6.5.2':
resolution: {integrity: sha512-/jJbwSTazlQEDOQw2FJ8LEEKVS72pU0lx6oM54kGpL8t/NJ2Jda3CZ4pcltiKTdqYSRk3ug1B3pil1gsjA6+8Q==}
'@codemirror/lint@6.9.3':
resolution: {integrity: sha512-y3YkYhdnhjDBAe0VIA0c4wVoFOvnp8CnAvfLqi0TqotIv92wIlAAP7HELOpLBsKwjAX6W92rSflA6an/2zBvXw==}
'@codemirror/search@6.6.0':
resolution: {integrity: sha512-koFuNXcDvyyotWcgOnZGmY7LZqEOXZaaxD/j6n18TCLx2/9HieZJ5H6hs1g8FiRxBD0DNfs0nXn17g872RmYdw==}
'@codemirror/state@6.5.4':
resolution: {integrity: sha512-8y7xqG/hpB53l25CIoit9/ngxdfoG+fx+V3SHBrinnhOtLvKHRyAJJuHzkWrR4YXXLX8eXBsejgAAxHUOdW1yw==}
'@codemirror/theme-one-dark@6.1.3':
resolution: {integrity: sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==}
'@codemirror/view@6.39.11':
resolution: {integrity: sha512-bWdeR8gWM87l4DB/kYSF9A+dVackzDb/V56Tq7QVrQ7rn86W0rgZFtlL3g3pem6AeGcb9NQNoy3ao4WpW4h5tQ==}
'@csstools/color-helpers@5.1.0': '@csstools/color-helpers@5.1.0':
resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -425,6 +587,60 @@ packages:
'@jridgewell/trace-mapping@0.3.31': '@jridgewell/trace-mapping@0.3.31':
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
'@lezer/common@1.5.0':
resolution: {integrity: sha512-PNGcolp9hr4PJdXR4ix7XtixDrClScvtSCYW3rQG106oVMOOI+jFb+0+J3mbeL/53g1Zd6s0kJzaw6Ri68GmAA==}
'@lezer/cpp@1.1.5':
resolution: {integrity: sha512-DIhSXmYtJKLehrjzDFN+2cPt547ySQ41nA8yqcDf/GxMc+YM736xqltFkvADL2M0VebU5I+3+4ks2Vv+Kyq3Aw==}
'@lezer/css@1.3.0':
resolution: {integrity: sha512-pBL7hup88KbI7hXnZV3PQsn43DHy6TWyzuyk2AO9UyoXcDltvIdqWKE1dLL/45JVZ+YZkHe1WVHqO6wugZZWcw==}
'@lezer/go@1.0.1':
resolution: {integrity: sha512-xToRsYxwsgJNHTgNdStpcvmbVuKxTapV0dM0wey1geMMRc9aggoVyKgzYp41D2/vVOx+Ii4hmE206kvxIXBVXQ==}
'@lezer/highlight@1.2.3':
resolution: {integrity: sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==}
'@lezer/html@1.3.13':
resolution: {integrity: sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg==}
'@lezer/java@1.1.3':
resolution: {integrity: sha512-yHquUfujwg6Yu4Fd1GNHCvidIvJwi/1Xu2DaKl/pfWIA2c1oXkVvawH3NyXhCaFx4OdlYBVX5wvz2f7Aoa/4Xw==}
'@lezer/javascript@1.5.4':
resolution: {integrity: sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==}
'@lezer/json@1.0.3':
resolution: {integrity: sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==}
'@lezer/lr@1.4.8':
resolution: {integrity: sha512-bPWa0Pgx69ylNlMlPvBPryqeLYQjyJjqPx+Aupm5zydLIF3NE+6MMLT8Yi23Bd9cif9VS00aUebn+6fDIGBcDA==}
'@lezer/markdown@1.6.3':
resolution: {integrity: sha512-jpGm5Ps+XErS+xA4urw7ogEGkeZOahVQF21Z6oECF0sj+2liwZopd2+I8uH5I/vZsRuuze3OxBREIANLf6KKUw==}
'@lezer/php@1.0.5':
resolution: {integrity: sha512-W7asp9DhM6q0W6DYNwIkLSKOvxlXRrif+UXBMxzsJUuqmhE7oVU+gS3THO4S/Puh7Xzgm858UNaFi6dxTP8dJA==}
'@lezer/python@1.1.18':
resolution: {integrity: sha512-31FiUrU7z9+d/ElGQLJFXl+dKOdx0jALlP3KEOsGTex8mvj+SoE1FgItcHWK/axkxCHGUSpqIHt6JAWfWu9Rhg==}
'@lezer/rust@1.0.2':
resolution: {integrity: sha512-Lz5sIPBdF2FUXcWeCu1//ojFAZqzTQNRga0aYv6dYXqJqPfMdCAI0NzajWUd4Xijj1IKJLtjoXRPMvTKWBcqKg==}
'@lezer/sass@1.1.0':
resolution: {integrity: sha512-3mMGdCTUZ/84ArHOuXWQr37pnf7f+Nw9ycPUeKX+wu19b7pSMcZGLbaXwvD2APMBDOGxPmpK/O6S1v1EvLoqgQ==}
'@lezer/xml@1.0.6':
resolution: {integrity: sha512-CdDwirL0OEaStFue/66ZmFSeppuL6Dwjlk8qk153mSQwiSH/Dlri4GNymrNWnUmPl2Um7QfV1FO9KFUyX3Twww==}
'@lezer/yaml@1.0.3':
resolution: {integrity: sha512-GuBLekbw9jDBDhGur82nuwkxKQ+a3W5H0GfaAthDXcAu+XdpS43VlnxA9E9hllkpSP5ellRDKjLLj7Lu9Wr6xA==}
'@marijn/find-cluster-break@1.0.2':
resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==}
'@polka/url@1.0.0-next.29': '@polka/url@1.0.0-next.29':
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
@@ -1015,6 +1231,9 @@ packages:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'} engines: {node: '>=6'}
codemirror@6.0.2:
resolution: {integrity: sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==}
color-convert@2.0.1: color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'} engines: {node: '>=7.0.0'}
@@ -1029,6 +1248,9 @@ packages:
resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
crelt@1.0.6:
resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==}
cross-spawn@7.0.6: cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
@@ -1663,6 +1885,9 @@ packages:
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
engines: {node: '>=8'} engines: {node: '>=8'}
style-mod@4.1.3:
resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==}
supports-color@7.2.0: supports-color@7.2.0:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -1842,6 +2067,9 @@ packages:
jsdom: jsdom:
optional: true optional: true
w3c-keyname@2.2.8:
resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
w3c-xmlserializer@5.0.0: w3c-xmlserializer@5.0.0:
resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -1949,6 +2177,216 @@ snapshots:
'@bcoe/v8-coverage@1.0.2': {} '@bcoe/v8-coverage@1.0.2': {}
'@codemirror/autocomplete@6.20.0':
dependencies:
'@codemirror/language': 6.12.1
'@codemirror/state': 6.5.4
'@codemirror/view': 6.39.11
'@lezer/common': 1.5.0
'@codemirror/commands@6.10.1':
dependencies:
'@codemirror/language': 6.12.1
'@codemirror/state': 6.5.4
'@codemirror/view': 6.39.11
'@lezer/common': 1.5.0
'@codemirror/lang-angular@0.1.4':
dependencies:
'@codemirror/lang-html': 6.4.11
'@codemirror/lang-javascript': 6.2.4
'@codemirror/language': 6.12.1
'@lezer/common': 1.5.0
'@lezer/highlight': 1.2.3
'@lezer/lr': 1.4.8
'@codemirror/lang-cpp@6.0.3':
dependencies:
'@codemirror/language': 6.12.1
'@lezer/cpp': 1.1.5
'@codemirror/lang-css@6.3.1':
dependencies:
'@codemirror/autocomplete': 6.20.0
'@codemirror/language': 6.12.1
'@codemirror/state': 6.5.4
'@lezer/common': 1.5.0
'@lezer/css': 1.3.0
'@codemirror/lang-go@6.0.1':
dependencies:
'@codemirror/autocomplete': 6.20.0
'@codemirror/language': 6.12.1
'@codemirror/state': 6.5.4
'@lezer/common': 1.5.0
'@lezer/go': 1.0.1
'@codemirror/lang-html@6.4.11':
dependencies:
'@codemirror/autocomplete': 6.20.0
'@codemirror/lang-css': 6.3.1
'@codemirror/lang-javascript': 6.2.4
'@codemirror/language': 6.12.1
'@codemirror/state': 6.5.4
'@codemirror/view': 6.39.11
'@lezer/common': 1.5.0
'@lezer/css': 1.3.0
'@lezer/html': 1.3.13
'@codemirror/lang-java@6.0.2':
dependencies:
'@codemirror/language': 6.12.1
'@lezer/java': 1.1.3
'@codemirror/lang-javascript@6.2.4':
dependencies:
'@codemirror/autocomplete': 6.20.0
'@codemirror/language': 6.12.1
'@codemirror/lint': 6.9.3
'@codemirror/state': 6.5.4
'@codemirror/view': 6.39.11
'@lezer/common': 1.5.0
'@lezer/javascript': 1.5.4
'@codemirror/lang-json@6.0.2':
dependencies:
'@codemirror/language': 6.12.1
'@lezer/json': 1.0.3
'@codemirror/lang-less@6.0.2':
dependencies:
'@codemirror/lang-css': 6.3.1
'@codemirror/language': 6.12.1
'@lezer/common': 1.5.0
'@lezer/highlight': 1.2.3
'@lezer/lr': 1.4.8
'@codemirror/lang-markdown@6.5.0':
dependencies:
'@codemirror/autocomplete': 6.20.0
'@codemirror/lang-html': 6.4.11
'@codemirror/language': 6.12.1
'@codemirror/state': 6.5.4
'@codemirror/view': 6.39.11
'@lezer/common': 1.5.0
'@lezer/markdown': 1.6.3
'@codemirror/lang-php@6.0.2':
dependencies:
'@codemirror/lang-html': 6.4.11
'@codemirror/language': 6.12.1
'@codemirror/state': 6.5.4
'@lezer/common': 1.5.0
'@lezer/php': 1.0.5
'@codemirror/lang-python@6.2.1':
dependencies:
'@codemirror/autocomplete': 6.20.0
'@codemirror/language': 6.12.1
'@codemirror/state': 6.5.4
'@lezer/common': 1.5.0
'@lezer/python': 1.1.18
'@codemirror/lang-rust@6.0.2':
dependencies:
'@codemirror/language': 6.12.1
'@lezer/rust': 1.0.2
'@codemirror/lang-sass@6.0.2':
dependencies:
'@codemirror/lang-css': 6.3.1
'@codemirror/language': 6.12.1
'@codemirror/state': 6.5.4
'@lezer/common': 1.5.0
'@lezer/sass': 1.1.0
'@codemirror/lang-sql@6.10.0':
dependencies:
'@codemirror/autocomplete': 6.20.0
'@codemirror/language': 6.12.1
'@codemirror/state': 6.5.4
'@lezer/common': 1.5.0
'@lezer/highlight': 1.2.3
'@lezer/lr': 1.4.8
'@codemirror/lang-vue@0.1.3':
dependencies:
'@codemirror/lang-html': 6.4.11
'@codemirror/lang-javascript': 6.2.4
'@codemirror/language': 6.12.1
'@lezer/common': 1.5.0
'@lezer/highlight': 1.2.3
'@lezer/lr': 1.4.8
'@codemirror/lang-wast@6.0.2':
dependencies:
'@codemirror/language': 6.12.1
'@lezer/common': 1.5.0
'@lezer/highlight': 1.2.3
'@lezer/lr': 1.4.8
'@codemirror/lang-xml@6.1.0':
dependencies:
'@codemirror/autocomplete': 6.20.0
'@codemirror/language': 6.12.1
'@codemirror/state': 6.5.4
'@codemirror/view': 6.39.11
'@lezer/common': 1.5.0
'@lezer/xml': 1.0.6
'@codemirror/lang-yaml@6.1.2':
dependencies:
'@codemirror/autocomplete': 6.20.0
'@codemirror/language': 6.12.1
'@codemirror/state': 6.5.4
'@lezer/common': 1.5.0
'@lezer/highlight': 1.2.3
'@lezer/lr': 1.4.8
'@lezer/yaml': 1.0.3
'@codemirror/language@6.12.1':
dependencies:
'@codemirror/state': 6.5.4
'@codemirror/view': 6.39.11
'@lezer/common': 1.5.0
'@lezer/highlight': 1.2.3
'@lezer/lr': 1.4.8
style-mod: 4.1.3
'@codemirror/legacy-modes@6.5.2':
dependencies:
'@codemirror/language': 6.12.1
'@codemirror/lint@6.9.3':
dependencies:
'@codemirror/state': 6.5.4
'@codemirror/view': 6.39.11
crelt: 1.0.6
'@codemirror/search@6.6.0':
dependencies:
'@codemirror/state': 6.5.4
'@codemirror/view': 6.39.11
crelt: 1.0.6
'@codemirror/state@6.5.4':
dependencies:
'@marijn/find-cluster-break': 1.0.2
'@codemirror/theme-one-dark@6.1.3':
dependencies:
'@codemirror/language': 6.12.1
'@codemirror/state': 6.5.4
'@codemirror/view': 6.39.11
'@lezer/highlight': 1.2.3
'@codemirror/view@6.39.11':
dependencies:
'@codemirror/state': 6.5.4
crelt: 1.0.6
style-mod: 4.1.3
w3c-keyname: 2.2.8
'@csstools/color-helpers@5.1.0': {} '@csstools/color-helpers@5.1.0': {}
'@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)':
@@ -2127,6 +2565,101 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2 '@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/sourcemap-codec': 1.5.5
'@lezer/common@1.5.0': {}
'@lezer/cpp@1.1.5':
dependencies:
'@lezer/common': 1.5.0
'@lezer/highlight': 1.2.3
'@lezer/lr': 1.4.8
'@lezer/css@1.3.0':
dependencies:
'@lezer/common': 1.5.0
'@lezer/highlight': 1.2.3
'@lezer/lr': 1.4.8
'@lezer/go@1.0.1':
dependencies:
'@lezer/common': 1.5.0
'@lezer/highlight': 1.2.3
'@lezer/lr': 1.4.8
'@lezer/highlight@1.2.3':
dependencies:
'@lezer/common': 1.5.0
'@lezer/html@1.3.13':
dependencies:
'@lezer/common': 1.5.0
'@lezer/highlight': 1.2.3
'@lezer/lr': 1.4.8
'@lezer/java@1.1.3':
dependencies:
'@lezer/common': 1.5.0
'@lezer/highlight': 1.2.3
'@lezer/lr': 1.4.8
'@lezer/javascript@1.5.4':
dependencies:
'@lezer/common': 1.5.0
'@lezer/highlight': 1.2.3
'@lezer/lr': 1.4.8
'@lezer/json@1.0.3':
dependencies:
'@lezer/common': 1.5.0
'@lezer/highlight': 1.2.3
'@lezer/lr': 1.4.8
'@lezer/lr@1.4.8':
dependencies:
'@lezer/common': 1.5.0
'@lezer/markdown@1.6.3':
dependencies:
'@lezer/common': 1.5.0
'@lezer/highlight': 1.2.3
'@lezer/php@1.0.5':
dependencies:
'@lezer/common': 1.5.0
'@lezer/highlight': 1.2.3
'@lezer/lr': 1.4.8
'@lezer/python@1.1.18':
dependencies:
'@lezer/common': 1.5.0
'@lezer/highlight': 1.2.3
'@lezer/lr': 1.4.8
'@lezer/rust@1.0.2':
dependencies:
'@lezer/common': 1.5.0
'@lezer/highlight': 1.2.3
'@lezer/lr': 1.4.8
'@lezer/sass@1.1.0':
dependencies:
'@lezer/common': 1.5.0
'@lezer/highlight': 1.2.3
'@lezer/lr': 1.4.8
'@lezer/xml@1.0.6':
dependencies:
'@lezer/common': 1.5.0
'@lezer/highlight': 1.2.3
'@lezer/lr': 1.4.8
'@lezer/yaml@1.0.3':
dependencies:
'@lezer/common': 1.5.0
'@lezer/highlight': 1.2.3
'@lezer/lr': 1.4.8
'@marijn/find-cluster-break@1.0.2': {}
'@polka/url@1.0.0-next.29': {} '@polka/url@1.0.0-next.29': {}
'@rollup/rollup-android-arm-eabi@4.55.1': '@rollup/rollup-android-arm-eabi@4.55.1':
@@ -2678,6 +3211,16 @@ snapshots:
clsx@2.1.1: {} clsx@2.1.1: {}
codemirror@6.0.2:
dependencies:
'@codemirror/autocomplete': 6.20.0
'@codemirror/commands': 6.10.1
'@codemirror/language': 6.12.1
'@codemirror/lint': 6.9.3
'@codemirror/search': 6.6.0
'@codemirror/state': 6.5.4
'@codemirror/view': 6.39.11
color-convert@2.0.1: color-convert@2.0.1:
dependencies: dependencies:
color-name: 1.1.4 color-name: 1.1.4
@@ -2688,6 +3231,8 @@ snapshots:
cookie@0.6.0: {} cookie@0.6.0: {}
crelt@1.0.6: {}
cross-spawn@7.0.6: cross-spawn@7.0.6:
dependencies: dependencies:
path-key: 3.1.1 path-key: 3.1.1
@@ -3304,6 +3849,8 @@ snapshots:
strip-json-comments@3.1.1: {} strip-json-comments@3.1.1: {}
style-mod@4.1.3: {}
supports-color@7.2.0: supports-color@7.2.0:
dependencies: dependencies:
has-flag: 4.0.0 has-flag: 4.0.0
@@ -3463,6 +4010,8 @@ snapshots:
- tsx - tsx
- yaml - yaml
w3c-keyname@2.2.8: {}
w3c-xmlserializer@5.0.0: w3c-xmlserializer@5.0.0:
dependencies: dependencies:
xml-name-validator: 5.0.0 xml-name-validator: 5.0.0
+69
View File
@@ -394,6 +394,75 @@ pub async fn get_file_size(file_path: String) -> Result<u64, String> {
Ok(metadata.len()) Ok(metadata.len())
} }
// ==================== Editor File Operations ====================
#[derive(Debug, Clone, serde::Serialize)]
pub struct FileEntry {
pub name: String,
pub path: String,
#[serde(rename = "isDirectory")]
pub is_directory: bool,
}
#[tauri::command]
pub async fn list_directory(path: String) -> Result<Vec<FileEntry>, String> {
use std::fs;
use std::path::Path;
let dir_path = Path::new(&path);
if !dir_path.exists() {
return Err(format!("Directory does not exist: {}", path));
}
if !dir_path.is_dir() {
return Err(format!("Path is not a directory: {}", path));
}
let entries = fs::read_dir(dir_path)
.map_err(|e| format!("Failed to read directory: {}", e))?;
let mut file_entries = Vec::new();
for entry in entries {
let entry = entry.map_err(|e| format!("Failed to read entry: {}", e))?;
let path = entry.path();
let name = entry
.file_name()
.to_string_lossy()
.to_string();
// Skip hidden files by default (can be made configurable later)
if name.starts_with('.') {
continue;
}
file_entries.push(FileEntry {
name,
path: path.to_string_lossy().to_string(),
is_directory: path.is_dir(),
});
}
Ok(file_entries)
}
#[tauri::command]
pub async fn read_file_content(path: String) -> Result<String, String> {
use std::fs;
fs::read_to_string(&path)
.map_err(|e| format!("Failed to read file: {}", e))
}
#[tauri::command]
pub async fn write_file_content(path: String, content: String) -> Result<(), String> {
use std::fs;
fs::write(&path, content)
.map_err(|e| format!("Failed to write file: {}", e))
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
+3
View File
@@ -151,6 +151,9 @@ pub fn run() {
search_clipboard_entries, search_clipboard_entries,
get_clipboard_languages, get_clipboard_languages,
update_clipboard_language, update_clipboard_language,
list_directory,
read_file_content,
write_file_content,
]) ])
.run(tauri::generate_context!()) .run(tauri::generate_context!())
.expect("error while running tauri application"); .expect("error while running tauri application");
@@ -12,6 +12,7 @@
{ keys: ["Escape"], description: "Close modals and panels" }, { keys: ["Escape"], description: "Close modals and panels" },
{ keys: ["Ctrl", "L"], description: "Clear the terminal" }, { keys: ["Ctrl", "L"], description: "Clear the terminal" },
{ keys: ["Ctrl", ","], description: "Open settings" }, { keys: ["Ctrl", ","], description: "Open settings" },
{ keys: ["Ctrl", "E"], description: "Toggle file editor" },
{ keys: ["Ctrl", "Shift", "M"], description: "Toggle compact mode" }, { keys: ["Ctrl", "Shift", "M"], description: "Toggle compact mode" },
{ keys: ["Ctrl", "Shift", "S"], description: "Toggle streamer mode" }, { keys: ["Ctrl", "Shift", "S"], description: "Toggle streamer mode" },
], ],
@@ -26,6 +27,15 @@
{ keys: ["↓"], description: "Next input from history" }, { keys: ["↓"], description: "Next input from history" },
], ],
}, },
{
category: "File Editor",
items: [
{ keys: ["Ctrl", "E"], description: "Toggle editor view" },
{ keys: ["Ctrl", "B"], description: "Toggle file browser" },
{ keys: ["Ctrl", "S"], description: "Save current file" },
{ keys: ["Ctrl", "W"], description: "Close current tab" },
],
},
{ {
category: "Slash Commands", category: "Slash Commands",
items: [ items: [
+29
View File
@@ -13,6 +13,7 @@
import { get } from "svelte/store"; import { get } from "svelte/store";
import { claudeStore } from "$lib/stores/claude"; import { claudeStore } from "$lib/stores/claude";
import { configStore, type HikariConfig, isStreamerMode } from "$lib/stores/config"; import { configStore, type HikariConfig, isStreamerMode } from "$lib/stores/config";
import { editorStore } from "$lib/stores/editor";
import type { ConnectionStatus } from "$lib/types/messages"; import type { ConnectionStatus } from "$lib/types/messages";
import { onMount } from "svelte"; import { onMount } from "svelte";
import StatsDisplay from "./StatsDisplay.svelte"; import StatsDisplay from "./StatsDisplay.svelte";
@@ -80,6 +81,15 @@
streamerModeActive = value; streamerModeActive = value;
}); });
let editorVisible = $state(false);
editorStore.isEditorVisible.subscribe((value) => {
editorVisible = value;
});
function toggleEditor() {
editorStore.toggleEditor();
}
onMount(async () => { onMount(async () => {
appVersion = await getVersion(); appVersion = await getVersion();
}); });
@@ -307,6 +317,25 @@
/> />
</svg> </svg>
</button> </button>
<button
onclick={toggleEditor}
disabled={connectionStatus !== "connected"}
class="p-1 text-gray-500 icon-trans-hover {editorVisible
? 'text-[var(--trans-pink)]'
: ''} disabled:opacity-40 disabled:cursor-not-allowed"
title={connectionStatus === "connected"
? "File Editor (Ctrl+E)"
: "Connect to enable file editor"}
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
/>
</svg>
</button>
<button <button
onclick={() => (showStats = !showStats)} onclick={() => (showStats = !showStats)}
class="p-1 text-gray-500 icon-trans-hover {showStats ? 'text-[var(--trans-pink)]' : ''}" class="p-1 text-gray-500 icon-trans-hover {showStats ? 'text-[var(--trans-pink)]' : ''}"
+179
View File
@@ -0,0 +1,179 @@
<script lang="ts">
import { onMount, onDestroy } from "svelte";
import { EditorView, basicSetup } from "codemirror";
import { EditorState } from "@codemirror/state";
import { keymap } from "@codemirror/view";
import { oneDark } from "@codemirror/theme-one-dark";
import { javascript } from "@codemirror/lang-javascript";
import { python } from "@codemirror/lang-python";
import { rust } from "@codemirror/lang-rust";
import { html } from "@codemirror/lang-html";
import { css } from "@codemirror/lang-css";
import { json } from "@codemirror/lang-json";
import { markdown } from "@codemirror/lang-markdown";
import { xml } from "@codemirror/lang-xml";
import { sql } from "@codemirror/lang-sql";
import { java } from "@codemirror/lang-java";
import { cpp } from "@codemirror/lang-cpp";
import { php } from "@codemirror/lang-php";
import { go } from "@codemirror/lang-go";
import { yaml } from "@codemirror/lang-yaml";
import { sass } from "@codemirror/lang-sass";
import { less } from "@codemirror/lang-less";
import { vue } from "@codemirror/lang-vue";
import { angular } from "@codemirror/lang-angular";
import { wast } from "@codemirror/lang-wast";
import { StreamLanguage } from "@codemirror/language";
import { shell } from "@codemirror/legacy-modes/mode/shell";
import { ruby } from "@codemirror/legacy-modes/mode/ruby";
import { swift } from "@codemirror/legacy-modes/mode/swift";
import { lua } from "@codemirror/legacy-modes/mode/lua";
import { r } from "@codemirror/legacy-modes/mode/r";
import { toml } from "@codemirror/legacy-modes/mode/toml";
import { dockerFile } from "@codemirror/legacy-modes/mode/dockerfile";
import { powerShell } from "@codemirror/legacy-modes/mode/powershell";
import { editorStore } from "$lib/stores/editor";
import type { EditorTab } from "$lib/types/editor";
import type { Extension } from "@codemirror/state";
export let tab: EditorTab;
let editorContainer: HTMLDivElement;
let view: EditorView | null = null;
function getLanguageExtension(language: string): Extension {
const languageMap: Record<string, () => Extension> = {
javascript: () => javascript({ jsx: true, typescript: false }),
typescript: () => javascript({ jsx: true, typescript: true }),
python: () => python(),
rust: () => rust(),
html: () => html(),
css: () => css(),
json: () => json(),
markdown: () => markdown(),
xml: () => xml(),
sql: () => sql(),
java: () => java(),
c: () => cpp(),
cpp: () => cpp(),
csharp: () => cpp(),
php: () => php(),
go: () => go(),
yaml: () => yaml(),
scss: () => sass(),
sass: () => sass(),
less: () => less(),
vue: () => vue(),
angular: () => angular(),
wasm: () => wast(),
shell: () => StreamLanguage.define(shell),
ruby: () => StreamLanguage.define(ruby),
swift: () => StreamLanguage.define(swift),
lua: () => StreamLanguage.define(lua),
r: () => StreamLanguage.define(r),
toml: () => StreamLanguage.define(toml),
dockerfile: () => StreamLanguage.define(dockerFile),
powershell: () => StreamLanguage.define(powerShell),
svelte: () => html(),
};
const getExtension = languageMap[language];
return getExtension ? getExtension() : [];
}
function createEditor() {
if (!editorContainer) return;
const saveKeymap = keymap.of([
{
key: "Mod-s",
run: () => {
editorStore.saveFile(tab.id);
return true;
},
},
]);
const updateListener = EditorView.updateListener.of((update) => {
if (update.docChanged) {
const content = update.state.doc.toString();
editorStore.updateTabContent(tab.id, content);
}
});
const state = EditorState.create({
doc: tab.content,
extensions: [
basicSetup,
oneDark,
getLanguageExtension(tab.language),
saveKeymap,
updateListener,
EditorView.theme({
"&": {
height: "100%",
fontSize: "14px",
},
".cm-scroller": {
overflow: "auto",
},
".cm-content": {
fontFamily: "'JetBrains Mono', 'Fira Code', 'Consolas', monospace",
},
".cm-gutters": {
backgroundColor: "var(--bg-secondary)",
borderRight: "1px solid var(--border-color)",
},
}),
],
});
view = new EditorView({
state,
parent: editorContainer,
});
}
function destroyEditor() {
if (view) {
view.destroy();
view = null;
}
}
onMount(() => {
createEditor();
});
onDestroy(() => {
destroyEditor();
});
$: if (view && tab.content !== view.state.doc.toString()) {
view.dispatch({
changes: {
from: 0,
to: view.state.doc.length,
insert: tab.content,
},
});
}
</script>
<div class="code-editor" bind:this={editorContainer}></div>
<style>
.code-editor {
flex: 1;
overflow: hidden;
background-color: var(--bg-terminal);
}
.code-editor :global(.cm-editor) {
height: 100%;
}
.code-editor :global(.cm-focused) {
outline: none;
}
</style>
@@ -0,0 +1,253 @@
<script lang="ts">
import { editorStore } from "$lib/stores/editor";
import FileBrowser from "./FileBrowser.svelte";
import EditorTabs from "./EditorTabs.svelte";
import CodeEditor from "./CodeEditor.svelte";
const isFileBrowserOpen = editorStore.isFileBrowserOpen;
const activeTab = editorStore.activeTab;
const saveError = editorStore.saveError;
function toggleFileBrowser() {
editorStore.toggleFileBrowser();
}
async function handleSave() {
try {
await editorStore.saveFile();
} catch {
// Error is already set in the store
}
}
</script>
<div class="editor-panel">
<div class="toolbar">
<button
class="toolbar-button"
class:active={$isFileBrowserOpen}
on:click={toggleFileBrowser}
title="Toggle file browser (Ctrl+B)"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" />
</svg>
</button>
<div class="toolbar-spacer"></div>
{#if $activeTab}
<button class="toolbar-button" on:click={handleSave} title="Save (Ctrl+S)">
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z" />
<polyline points="17 21 17 13 7 13 7 21" />
<polyline points="7 3 7 8 15 8" />
</svg>
</button>
{/if}
</div>
{#if $saveError}
<div class="error-banner">
<span>{$saveError}</span>
<button
class="dismiss-button"
on:click={() => {}}
title="Dismiss error"
aria-label="Dismiss error"
>
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M18 6L6 18M6 6l12 12" />
</svg>
</button>
</div>
{/if}
<div class="editor-content">
{#if $isFileBrowserOpen}
<div class="file-browser-container">
<FileBrowser />
</div>
{/if}
<div class="editor-main">
<EditorTabs />
<div class="editor-area">
{#if $activeTab}
{#key $activeTab.id}
<CodeEditor tab={$activeTab} />
{/key}
{:else}
<div class="no-file">
<div class="no-file-content">
<svg
width="48"
height="48"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1"
>
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
<polyline points="14 2 14 8 20 8" />
</svg>
<p>Select a file to edit</p>
<p class="hint">Use the file browser on the left or press Ctrl+B to toggle it</p>
</div>
</div>
{/if}
</div>
</div>
</div>
</div>
<style>
.editor-panel {
display: flex;
flex-direction: column;
height: 100%;
background-color: var(--bg-primary);
}
.toolbar {
display: flex;
align-items: center;
gap: 4px;
padding: 4px 8px;
background-color: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
}
.toolbar-button {
display: flex;
align-items: center;
justify-content: center;
padding: 6px;
background: transparent;
border: none;
color: var(--text-secondary);
cursor: pointer;
border-radius: 4px;
transition: all 0.15s ease;
}
.toolbar-button:hover {
background-color: var(--bg-primary);
color: var(--text-primary);
}
.toolbar-button.active {
background-color: var(--bg-primary);
color: var(--accent-primary);
}
.toolbar-spacer {
flex: 1;
}
.error-banner {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
background-color: #ff000022;
border-bottom: 1px solid #ff0000;
color: #ff6b6b;
font-size: 13px;
}
.dismiss-button {
display: flex;
align-items: center;
justify-content: center;
padding: 4px;
background: transparent;
border: none;
color: #ff6b6b;
cursor: pointer;
border-radius: 4px;
}
.dismiss-button:hover {
background-color: #ff000033;
}
.editor-content {
display: flex;
flex: 1;
overflow: hidden;
}
.file-browser-container {
width: 250px;
min-width: 150px;
max-width: 400px;
flex-shrink: 0;
overflow: hidden;
}
.editor-main {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.editor-area {
flex: 1;
overflow: hidden;
display: flex;
}
.no-file {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--bg-terminal);
}
.no-file-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
color: var(--text-secondary);
text-align: center;
}
.no-file-content svg {
opacity: 0.5;
}
.no-file-content p {
margin: 0;
}
.no-file-content .hint {
font-size: 12px;
opacity: 0.7;
}
</style>
+170
View File
@@ -0,0 +1,170 @@
<script lang="ts">
import { editorStore } from "$lib/stores/editor";
const tabs = editorStore.tabs;
const activeTabId = editorStore.activeTabId;
function handleTabClick(tabId: string) {
editorStore.setActiveTab(tabId);
}
function handleCloseTab(event: MouseEvent, tabId: string) {
event.stopPropagation();
editorStore.closeTab(tabId);
}
function handleKeydown(event: KeyboardEvent, tabId: string) {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
editorStore.setActiveTab(tabId);
}
}
function handleCloseKeydown(event: KeyboardEvent, tabId: string) {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
event.stopPropagation();
editorStore.closeTab(tabId);
}
}
</script>
<div class="editor-tabs">
{#if $tabs.length === 0}
<div class="no-tabs">No files open</div>
{:else}
<div class="tabs-container" role="tablist">
{#each $tabs as tab (tab.id)}
<div
class="tab"
class:active={tab.id === $activeTabId}
class:dirty={tab.isDirty}
role="tab"
tabindex="0"
aria-selected={tab.id === $activeTabId}
on:click={() => handleTabClick(tab.id)}
on:keydown={(e) => handleKeydown(e, tab.id)}
title={tab.filePath}
>
<span class="tab-name">
{tab.fileName}
{#if tab.isDirty}
<span class="dirty-indicator">*</span>
{/if}
</span>
<button
class="close-button"
on:click={(e) => handleCloseTab(e, tab.id)}
on:keydown={(e) => handleCloseKeydown(e, tab.id)}
title="Close tab"
aria-label="Close {tab.fileName}"
>
<svg
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M18 6L6 18M6 6l12 12" />
</svg>
</button>
</div>
{/each}
</div>
{/if}
</div>
<style>
.editor-tabs {
display: flex;
align-items: center;
background-color: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
min-height: 36px;
}
.no-tabs {
padding: 8px 12px;
color: var(--text-secondary);
font-size: 13px;
}
.tabs-container {
display: flex;
overflow-x: auto;
scrollbar-width: thin;
}
.tab {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: transparent;
border: none;
border-right: 1px solid var(--border-color);
color: var(--text-secondary);
font-size: 13px;
cursor: pointer;
white-space: nowrap;
transition: all 0.15s ease;
}
.tab:hover {
background-color: var(--bg-primary);
color: var(--text-primary);
}
.tab:focus {
outline: 1px solid var(--accent-primary);
outline-offset: -1px;
}
.tab.active {
background-color: var(--bg-terminal);
color: var(--text-primary);
border-bottom: 2px solid var(--accent-primary);
margin-bottom: -1px;
}
.tab-name {
display: flex;
align-items: center;
gap: 2px;
}
.dirty-indicator {
color: var(--accent-primary);
font-weight: bold;
}
.close-button {
display: flex;
align-items: center;
justify-content: center;
padding: 2px;
background: transparent;
border: none;
color: var(--text-secondary);
cursor: pointer;
border-radius: 4px;
opacity: 0;
transition: all 0.15s ease;
}
.tab:hover .close-button,
.tab.active .close-button {
opacity: 1;
}
.close-button:hover {
background-color: var(--bg-secondary);
color: var(--text-primary);
}
.tab.dirty .close-button {
opacity: 1;
}
</style>
@@ -0,0 +1,151 @@
<script lang="ts">
import { editorStore } from "$lib/stores/editor";
import FileTreeItem from "./FileTreeItem.svelte";
const fileTree = editorStore.fileTree;
const isLoadingTree = editorStore.isLoadingTree;
const currentDirectory = editorStore.currentDirectory;
function handleRefresh() {
const dir = $currentDirectory;
if (dir) {
editorStore.initializeFileTree(dir);
}
}
</script>
<div class="file-browser">
<div class="header">
<span class="title">Files</span>
<button class="refresh-button" on:click={handleRefresh} title="Refresh file tree">
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M23 4v6h-6" />
<path d="M1 20v-6h6" />
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15" />
</svg>
</button>
</div>
<div class="tree-container">
{#if $isLoadingTree}
<div class="loading">
<svg
class="spinner"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<circle cx="12" cy="12" r="10" stroke-dasharray="32" stroke-dashoffset="32">
<animate
attributeName="stroke-dashoffset"
dur="1s"
values="32;0"
repeatCount="indefinite"
/>
</circle>
</svg>
<span>Loading...</span>
</div>
{:else if $fileTree.length === 0}
<div class="empty">
<span>No files found</span>
</div>
{:else}
<div class="tree">
{#each $fileTree as entry (entry.path)}
<FileTreeItem {entry} />
{/each}
</div>
{/if}
</div>
</div>
<style>
.file-browser {
display: flex;
flex-direction: column;
height: 100%;
background-color: var(--bg-primary);
border-right: 1px solid var(--border-color);
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
border-bottom: 1px solid var(--border-color);
background-color: var(--bg-secondary);
}
.title {
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
color: var(--text-secondary);
letter-spacing: 0.5px;
}
.refresh-button {
display: flex;
align-items: center;
justify-content: center;
padding: 4px;
background: transparent;
border: none;
color: var(--text-secondary);
cursor: pointer;
border-radius: 4px;
transition: all 0.15s ease;
}
.refresh-button:hover {
background-color: var(--bg-primary);
color: var(--text-primary);
}
.tree-container {
flex: 1;
overflow-y: auto;
padding: 4px 0;
}
.tree {
min-width: max-content;
}
.loading,
.empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
padding: 24px;
color: var(--text-secondary);
font-size: 13px;
}
.spinner {
animation: spin 1s linear infinite;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
</style>
@@ -0,0 +1,195 @@
<script lang="ts">
import type { FileEntry } from "$lib/types/editor";
import { editorStore } from "$lib/stores/editor";
export let entry: FileEntry;
export let depth: number = 0;
function handleClick() {
if (entry.isDirectory) {
editorStore.toggleDirectory(entry);
} else {
editorStore.openFile(entry.path);
}
}
function handleKeydown(event: KeyboardEvent) {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
handleClick();
}
}
$: isExpanded = entry.isExpanded ?? false;
$: isLoading = entry.isLoading ?? false;
$: children = entry.children ?? [];
</script>
<div class="file-tree-item">
<button
class="item-row"
class:directory={entry.isDirectory}
class:file={!entry.isDirectory}
style="padding-left: {depth * 16 + 8}px"
on:click={handleClick}
on:keydown={handleKeydown}
title={entry.path}
>
{#if entry.isDirectory}
<span class="icon">
{#if isLoading}
<svg
class="spinner"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<circle cx="12" cy="12" r="10" stroke-dasharray="32" stroke-dashoffset="32">
<animate
attributeName="stroke-dashoffset"
dur="1s"
values="32;0"
repeatCount="indefinite"
/>
</circle>
</svg>
{:else if isExpanded}
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M6 9l6 6 6-6" />
</svg>
{:else}
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M9 6l6 6-6 6" />
</svg>
{/if}
</span>
<span class="folder-icon">
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
<path
d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"
/>
</svg>
</span>
{:else}
<span class="icon spacer"></span>
<span class="file-icon">
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
<polyline points="14 2 14 8 20 8" />
</svg>
</span>
{/if}
<span class="name">{entry.name}</span>
</button>
{#if entry.isDirectory && isExpanded && children.length > 0}
<div class="children">
{#each children as child (child.path)}
<svelte:self entry={child} depth={depth + 1} />
{/each}
</div>
{/if}
</div>
<style>
.file-tree-item {
user-select: none;
}
.item-row {
display: flex;
align-items: center;
gap: 4px;
width: 100%;
padding: 4px 8px;
background: transparent;
border: none;
color: var(--text-primary);
font-size: 13px;
cursor: pointer;
text-align: left;
border-radius: 4px;
transition: background-color 0.15s ease;
}
.item-row:hover {
background-color: var(--bg-secondary);
}
.item-row:focus {
outline: 1px solid var(--accent-primary);
outline-offset: -1px;
}
.icon {
flex-shrink: 0;
width: 14px;
height: 14px;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-secondary);
}
.icon.spacer {
visibility: hidden;
}
.folder-icon {
flex-shrink: 0;
color: var(--accent-primary);
}
.file-icon {
flex-shrink: 0;
color: var(--text-secondary);
}
.name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.children {
margin-left: 0;
}
.spinner {
animation: spin 1s linear infinite;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
</style>
+285
View File
@@ -0,0 +1,285 @@
import { writable, derived, get } from "svelte/store";
import { invoke } from "@tauri-apps/api/core";
import type { EditorState, EditorTab, FileEntry } from "$lib/types/editor";
const defaultState: EditorState = {
tabs: [],
activeTabId: null,
isFileBrowserOpen: true,
currentDirectory: "",
fileTree: [],
isLoadingTree: false,
};
function getLanguageFromPath(filePath: string): string {
const ext = filePath.split(".").pop()?.toLowerCase() || "";
const languageMap: Record<string, string> = {
js: "javascript",
jsx: "javascript",
ts: "typescript",
tsx: "typescript",
py: "python",
rs: "rust",
go: "go",
java: "java",
c: "c",
cpp: "cpp",
h: "c",
hpp: "cpp",
cs: "csharp",
rb: "ruby",
php: "php",
swift: "swift",
kt: "kotlin",
scala: "scala",
html: "html",
htm: "html",
css: "css",
scss: "scss",
sass: "sass",
less: "less",
json: "json",
xml: "xml",
yaml: "yaml",
yml: "yaml",
toml: "toml",
md: "markdown",
markdown: "markdown",
sql: "sql",
sh: "shell",
bash: "shell",
zsh: "shell",
ps1: "powershell",
dockerfile: "dockerfile",
svelte: "svelte",
vue: "vue",
graphql: "graphql",
gql: "graphql",
lua: "lua",
r: "r",
dart: "dart",
elm: "elm",
ex: "elixir",
exs: "elixir",
erl: "erlang",
hs: "haskell",
clj: "clojure",
lisp: "lisp",
ml: "ocaml",
fs: "fsharp",
zig: "zig",
nim: "nim",
v: "v",
wasm: "wasm",
wat: "wasm",
};
return languageMap[ext] || "plaintext";
}
function generateTabId(): string {
return `tab-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
}
function createEditorStore() {
const state = writable<EditorState>(defaultState);
const isEditorVisible = writable<boolean>(false);
const saveError = writable<string | null>(null);
async function loadDirectory(dirPath: string): Promise<FileEntry[]> {
try {
const entries = await invoke<FileEntry[]>("list_directory", { path: dirPath });
return entries.sort((a, b) => {
if (a.isDirectory && !b.isDirectory) return -1;
if (!a.isDirectory && b.isDirectory) return 1;
return a.name.localeCompare(b.name);
});
} catch (error) {
console.error("Failed to load directory:", error);
return [];
}
}
async function initializeFileTree(rootPath: string) {
state.update((s) => ({ ...s, isLoadingTree: true, currentDirectory: rootPath }));
try {
const entries = await loadDirectory(rootPath);
state.update((s) => ({ ...s, fileTree: entries, isLoadingTree: false }));
} catch (error) {
console.error("Failed to initialize file tree:", error);
state.update((s) => ({ ...s, isLoadingTree: false }));
}
}
async function toggleDirectory(entry: FileEntry) {
if (!entry.isDirectory) return;
state.update((s) => {
const updateTree = (entries: FileEntry[]): FileEntry[] => {
return entries.map((e) => {
if (e.path === entry.path) {
return { ...e, isExpanded: !e.isExpanded, isLoading: !e.isExpanded && !e.children };
}
if (e.children) {
return { ...e, children: updateTree(e.children) };
}
return e;
});
};
return { ...s, fileTree: updateTree(s.fileTree) };
});
if (!entry.isExpanded && !entry.children) {
const children = await loadDirectory(entry.path);
state.update((s) => {
const updateTree = (entries: FileEntry[]): FileEntry[] => {
return entries.map((e) => {
if (e.path === entry.path) {
return { ...e, children, isLoading: false };
}
if (e.children) {
return { ...e, children: updateTree(e.children) };
}
return e;
});
};
return { ...s, fileTree: updateTree(s.fileTree) };
});
}
}
async function openFile(filePath: string) {
const currentState = get(state);
const existingTab = currentState.tabs.find((t) => t.filePath === filePath);
if (existingTab) {
state.update((s) => ({ ...s, activeTabId: existingTab.id }));
return;
}
try {
const content = await invoke<string>("read_file_content", { path: filePath });
const fileName = filePath.split(/[/\\]/).pop() || "untitled";
const language = getLanguageFromPath(filePath);
const newTab: EditorTab = {
id: generateTabId(),
filePath,
fileName,
content,
originalContent: content,
isDirty: false,
language,
};
state.update((s) => ({
...s,
tabs: [...s.tabs, newTab],
activeTabId: newTab.id,
}));
} catch (error) {
console.error("Failed to open file:", error);
saveError.set(`Failed to open file: ${error}`);
}
}
async function saveFile(tabId?: string) {
const currentState = get(state);
const tab = tabId
? currentState.tabs.find((t) => t.id === tabId)
: currentState.tabs.find((t) => t.id === currentState.activeTabId);
if (!tab) return;
saveError.set(null);
try {
await invoke("write_file_content", { path: tab.filePath, content: tab.content });
state.update((s) => ({
...s,
tabs: s.tabs.map((t) =>
t.id === tab.id ? { ...t, originalContent: t.content, isDirty: false } : t
),
}));
} catch (error) {
console.error("Failed to save file:", error);
saveError.set(`Failed to save file: ${error}`);
throw error;
}
}
function updateTabContent(tabId: string, content: string) {
state.update((s) => ({
...s,
tabs: s.tabs.map((t) =>
t.id === tabId ? { ...t, content, isDirty: content !== t.originalContent } : t
),
}));
}
function closeTab(tabId: string) {
state.update((s) => {
const tabIndex = s.tabs.findIndex((t) => t.id === tabId);
const newTabs = s.tabs.filter((t) => t.id !== tabId);
let newActiveId = s.activeTabId;
if (s.activeTabId === tabId) {
if (newTabs.length === 0) {
newActiveId = null;
} else if (tabIndex >= newTabs.length) {
newActiveId = newTabs[newTabs.length - 1].id;
} else {
newActiveId = newTabs[tabIndex].id;
}
}
return { ...s, tabs: newTabs, activeTabId: newActiveId };
});
}
function setActiveTab(tabId: string) {
state.update((s) => ({ ...s, activeTabId: tabId }));
}
function toggleFileBrowser() {
state.update((s) => ({ ...s, isFileBrowserOpen: !s.isFileBrowserOpen }));
}
function toggleEditor() {
isEditorVisible.update((v) => !v);
}
function showEditor() {
isEditorVisible.set(true);
}
function hideEditor() {
isEditorVisible.set(false);
}
return {
state: { subscribe: state.subscribe },
isEditorVisible: { subscribe: isEditorVisible.subscribe },
saveError: { subscribe: saveError.subscribe },
initializeFileTree,
toggleDirectory,
openFile,
saveFile,
updateTabContent,
closeTab,
setActiveTab,
toggleFileBrowser,
toggleEditor,
showEditor,
hideEditor,
tabs: derived(state, ($state) => $state.tabs),
activeTab: derived(state, ($state) => $state.tabs.find((t) => t.id === $state.activeTabId)),
activeTabId: derived(state, ($state) => $state.activeTabId),
fileTree: derived(state, ($state) => $state.fileTree),
isFileBrowserOpen: derived(state, ($state) => $state.isFileBrowserOpen),
isLoadingTree: derived(state, ($state) => $state.isLoadingTree),
currentDirectory: derived(state, ($state) => $state.currentDirectory),
hasDirtyTabs: derived(state, ($state) => $state.tabs.some((t) => t.isDirty)),
};
}
export const editorStore = createEditorStore();
+27
View File
@@ -0,0 +1,27 @@
export interface FileEntry {
name: string;
path: string;
isDirectory: boolean;
isExpanded?: boolean;
children?: FileEntry[];
isLoading?: boolean;
}
export interface EditorTab {
id: string;
filePath: string;
fileName: string;
content: string;
originalContent: string;
isDirty: boolean;
language: string;
}
export interface EditorState {
tabs: EditorTab[];
activeTabId: string | null;
isFileBrowserOpen: boolean;
currentDirectory: string;
fileTree: FileEntry[];
isLoadingTree: boolean;
}
+69 -3
View File
@@ -7,6 +7,7 @@
import { initNotificationSync, cleanupNotificationSync } from "$lib/stores/notifications"; import { initNotificationSync, cleanupNotificationSync } from "$lib/stores/notifications";
import { conversationsStore } from "$lib/stores/conversations"; import { conversationsStore } from "$lib/stores/conversations";
import { claudeStore, isClaudeProcessing } from "$lib/stores/claude"; import { claudeStore, isClaudeProcessing } from "$lib/stores/claude";
import { editorStore } from "$lib/stores/editor";
import { getCurrentWindow, LogicalSize } from "@tauri-apps/api/window"; import { getCurrentWindow, LogicalSize } from "@tauri-apps/api/window";
import "$lib/notifications/testNotifications"; import "$lib/notifications/testNotifications";
import Terminal from "$lib/components/Terminal.svelte"; import Terminal from "$lib/components/Terminal.svelte";
@@ -14,6 +15,7 @@
import StatusBar from "$lib/components/StatusBar.svelte"; import StatusBar from "$lib/components/StatusBar.svelte";
import AnimeGirl from "$lib/components/AnimeGirl.svelte"; import AnimeGirl from "$lib/components/AnimeGirl.svelte";
import CompactMode from "$lib/components/CompactMode.svelte"; import CompactMode from "$lib/components/CompactMode.svelte";
import EditorPanel from "$lib/components/editor/EditorPanel.svelte";
import { characterState } from "$lib/stores/character"; import { characterState } from "$lib/stores/character";
import type { CharacterState } from "$lib/types/states"; import type { CharacterState } from "$lib/types/states";
import PermissionModal from "$lib/components/PermissionModal.svelte"; import PermissionModal from "$lib/components/PermissionModal.svelte";
@@ -29,6 +31,32 @@
let currentCharacterState: CharacterState = $state("idle"); let currentCharacterState: CharacterState = $state("idle");
let compactModeActive = $state(false); let compactModeActive = $state(false);
// Editor state
const isEditorVisible = editorStore.isEditorVisible;
let lastInitializedCwd = "";
// Track connection status and CWD for the editor
const connectionStatus = claudeStore.connectionStatus;
const currentWorkingDirectory = claudeStore.currentWorkingDirectory;
// Initialize/update editor file tree when CWD changes while editor is visible
$effect(() => {
const visible = $isEditorVisible;
const cwd = $currentWorkingDirectory;
const connected = $connectionStatus === "connected";
// Only initialize when editor is visible, connected, and CWD is set
if (visible && connected && cwd && cwd !== lastInitializedCwd) {
lastInitializedCwd = cwd;
editorStore.initializeFileTree(cwd);
}
// Hide editor if disconnected
if (!connected && visible) {
editorStore.hideEditor();
}
});
// Window size constants // Window size constants
const COMPACT_WIDTH = 280; const COMPACT_WIDTH = 280;
const COMPACT_HEIGHT = 400; const COMPACT_HEIGHT = 400;
@@ -176,6 +204,40 @@
toggleCompactMode(); toggleCompactMode();
return; return;
} }
// Ctrl+E - Toggle editor panel (only when connected)
if (event.ctrlKey && event.key === "e") {
event.preventDefault();
// Only allow opening the editor when connected
if (get(claudeStore.connectionStatus) === "connected") {
editorStore.toggleEditor();
}
return;
}
// Ctrl+B - Toggle file browser (when editor is visible)
if (event.ctrlKey && event.key === "b" && get(editorStore.isEditorVisible)) {
event.preventDefault();
editorStore.toggleFileBrowser();
return;
}
// Ctrl+S - Save current file (when editor is visible)
if (event.ctrlKey && event.key === "s" && get(editorStore.isEditorVisible)) {
event.preventDefault();
editorStore.saveFile();
return;
}
// Ctrl+W - Close current tab (when editor is visible)
if (event.ctrlKey && event.key === "w" && get(editorStore.isEditorVisible)) {
event.preventDefault();
const activeTabId = get(editorStore.activeTabId);
if (activeTabId) {
editorStore.closeTab(activeTabId);
}
return;
}
} }
async function handleInterrupt() { async function handleInterrupt() {
@@ -330,10 +392,14 @@
onmousedown={startResize} onmousedown={startResize}
></div> ></div>
<!-- Right panel: Terminal and input --> <!-- Right panel: Terminal/Editor and input -->
<div class="terminal-panel flex-1 flex flex-col min-w-0"> <div class="terminal-panel flex-1 flex flex-col min-w-0">
<Terminal /> {#if $isEditorVisible}
<InputBar /> <EditorPanel />
{:else}
<Terminal />
<InputBar />
{/if}
</div> </div>
</main> </main>