Compare commits
528 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3de9faed5c | |||
| c179912368 | |||
| 89c2adee70 | |||
| c4b9637546 | |||
| 868736330a | |||
| 2c856c9932 | |||
| a3b49d8aa2 | |||
| da9817bd99 | |||
| 58b4d90b49 | |||
| 39ea63b207 | |||
| b02b2f0c89 | |||
| 902d419026 | |||
| b244f2f08b | |||
| 4d9b52ede6 | |||
| e94d2de641 | |||
| 5fdc5d6df2 | |||
| cf5ff98081 | |||
| 45eeb14b54 | |||
| dcfb5d443c | |||
| f839e73ebc | |||
| c775d34d75 | |||
| 3023ed87d2 | |||
| 3a5a04ceae | |||
| 22475f7206 | |||
| 68333ce349 | |||
| 9f3dbc3eb4 | |||
| 10949b44d1 | |||
| 45778d5735 | |||
| 816d6090cc | |||
| 355d8d9a5a | |||
| b47e8f2237 | |||
| 0543b2c893 | |||
| 69cf58af5a | |||
| f9b3dbe316 | |||
| 176b988539 | |||
| 11cc9f8b4e | |||
| ab396d4c1d | |||
| a01ca97b18 | |||
| 1b4105ed2b | |||
| 4024ed40c2 | |||
| 8c53878681 | |||
| 7bd52438f5 | |||
| 6f2d513d3f | |||
| 800aeea247 | |||
| 021acf541a | |||
| 7a739e567c | |||
| ef52ebfdc2 | |||
| e83a172816 | |||
| c3ef7e616f | |||
| f77907fe0d | |||
| 6d28b0627f | |||
| 7d3ce48f5f | |||
| 97c611f4f2 | |||
| 90d94ecd3c | |||
| 9f7154ddab | |||
| 5cdb4b0c47 | |||
| a61fdd0419 | |||
| b7db527866 | |||
| c921059690 | |||
| cee6dcc524 | |||
| b9e097bfc0 | |||
| 87c6669020 | |||
| 80448c4471 | |||
| b4f9c1fa97 | |||
| 10052819ce | |||
| 0f2ebecbd3 | |||
| eb68e1d294 | |||
| 17aa9eea97 | |||
| 5e85d804f8 | |||
| 8f2a2d642f | |||
| 6141351e15 | |||
| 00e196a55f | |||
| 16913e96e5 | |||
| 6f55e6900b | |||
| 36e10dcc08 | |||
| ce8c790e5c | |||
| 738067951a | |||
| 8b1a42499b | |||
| 7fd52b4805 | |||
| 5f30506f46 | |||
| 48efc32dd3 | |||
| 5fffd2fe0f | |||
| 664428c8f0 | |||
| aa8b999da0 | |||
| d99d2cca4a | |||
| e7da1d27bd | |||
| 56c50dc218 | |||
| e0e7b360fe | |||
| 434a8289d9 | |||
| 7e0e65d3b7 | |||
| 4cb96cb60e | |||
| 4a7ea3ec43 | |||
| bd53772523 | |||
| f09f538b05 | |||
| ff2ed836c7 | |||
| 6db29526ec | |||
| 27d9cc2916 | |||
| dbbaa61961 | |||
| 05bd1790a7 | |||
| 4b30990e92 | |||
| 3a0eedc9fa | |||
| 64d4e264bf | |||
| afc5cf512b | |||
| 35e8cf59ff | |||
| fd4ecaff8a | |||
| 55c4f9e1cf | |||
| 78f9463da1 | |||
| 23f1e8c641 | |||
| 79eacad18b | |||
| 3e0e0bd4aa | |||
| fece3b2548 | |||
| 091855a89a | |||
| b3bd454bad | |||
| bd580946ce | |||
| 3bd6d08841 | |||
| 152cfd93aa | |||
| 7ea4e16f5f | |||
| 884548615b | |||
| b62943fffd | |||
| 9bd97d0cd5 | |||
| bf9852858e | |||
| 6058b7ff78 | |||
| 9b8f3ec2e2 | |||
| d3fd5be294 | |||
| aa4398c734 | |||
| 29d1a919be | |||
| feea5e13f3 | |||
| 119db73f69 | |||
| f59b24496e | |||
| a2ae9794ed | |||
| f335ce7204 | |||
| 0ddab03f8b | |||
| 5f8fddb01e | |||
| c81f628b7b | |||
| 9d1ab8728a | |||
| 7a0f9ac77c | |||
| a0fa104d23 | |||
| 7fd0b133e5 | |||
| 321876932a | |||
| eda8517c78 | |||
| 6ce37f7627 | |||
| 18b76885f5 | |||
| ba8644fbb6 | |||
| ee959ded08 | |||
| 499a47904e | |||
| 5ac03bb917 | |||
| 922f3ba2d7 | |||
| a189629341 | |||
| d2098f71eb | |||
| 77c59f2e80 | |||
| e6ecad3f65 | |||
| ee43ec99ea | |||
| e77039e9ad | |||
| 6aa1375067 | |||
| fe017948c6 | |||
| 9b3d4cbdba | |||
| a8cf0b361f | |||
| 680125baec | |||
| 0db4f7118d | |||
| 2ea20243a8 | |||
| 4d02381dab | |||
| 6990216fbe | |||
| f5d68d9348 | |||
| 165c5aac24 | |||
| 22f4a75c00 | |||
| 06513168c1 | |||
| a80883c10f | |||
| 20b0fd6af9 | |||
| 5171f7244f | |||
| 43708d25e7 | |||
| 2b54733242 | |||
| cd77d1a1c4 | |||
| 37af09add8 | |||
| ade1e70536 | |||
| 159e0b677f | |||
| 335ebe1e11 | |||
| 7be1ae5003 | |||
| 5d7f8fc91f | |||
| d6b3bdee35 | |||
| 8c1626f79b | |||
| 97ffe0b4cc | |||
| 9cf316f808 | |||
| aabacba2a9 | |||
| 39ccb1bbbd | |||
| 63560308b5 | |||
| 967aff2d19 | |||
| 50327a238e | |||
| 8eb5abe429 | |||
| 3c532478de | |||
| 5d5839736e | |||
| b8fd40e735 | |||
| a869ddb6f3 | |||
| e5a40e3d35 | |||
| 1081b8879f | |||
| 4efc5209ab | |||
| adcdeed094 | |||
| 86285e2393 | |||
| c1b6b2e01d | |||
| 2dcdd78316 | |||
| fa9a3db86f | |||
| 4445ff28ce | |||
| 9d6a4c4648 | |||
| e41f8790b5 | |||
| d363edb1a8 | |||
| 3cddc5d5cc | |||
| 2509290ab2 | |||
| aba49edfdd | |||
| 13b60f7ee3 | |||
| bd5cf464c2 | |||
| 44e7d10ece | |||
| 6bf79b27c0 | |||
| bb22aefb6a | |||
| 7002f320d6 | |||
| def8803919 | |||
| fdb21a68b8 | |||
| 455acc3490 | |||
| bc5b07b26f | |||
| 79ad57b112 | |||
| 9391dcb0b8 | |||
| b90716e38e | |||
| d8d21b0f25 | |||
| 3bf06bfe26 | |||
| 9166fa8684 | |||
| df2165403e | |||
| 44e0f0dfc7 | |||
| d6e496c396 | |||
| 7c94b95df4 | |||
| 85759eeb4a | |||
| 6230971dff | |||
| 8f08e40819 | |||
| ae635f0102 | |||
| cb8cc587f6 | |||
| 77f931c923 | |||
| 8dcb5d35df | |||
| 6483dcffa8 | |||
| ccaf6e2e3d | |||
| d5ae1a328c | |||
| 45c83ee661 | |||
| af7dbb4b70 | |||
| 72a6daaa58 | |||
| 898a5803dd | |||
| cffc6f6db9 | |||
| b2ba634cc4 | |||
| c62d6a6790 | |||
| f8bee1d4a2 | |||
| 41e66a73c4 | |||
| 5da0e4821f | |||
| 6aefdc3095 | |||
| 4055dc7775 | |||
| f48889627a | |||
| 3c59428fe8 | |||
| 8a8a982d21 | |||
| 7f890b946d | |||
| 1799ae9d84 | |||
| c7bdd2d64c | |||
| 5fde9194af | |||
| b6408512d3 | |||
| d5dc411e4c | |||
| e14fdd6678 | |||
| 8b5dfa66d5 | |||
| aef606dfb4 | |||
| 89b29ac97b | |||
| 908de32b8f | |||
| 4787400865 | |||
| 8b5e7dc25b | |||
| 7289f5f15a | |||
| 5e83d419ce | |||
| 3128000cf7 | |||
| b1626849ba | |||
| 99901d1e20 | |||
| 858faed92b | |||
| a4f94087df | |||
| 4236d920d9 | |||
| 56619c7b17 | |||
| ea65903b3f | |||
| 78671db5b8 | |||
| 7f38c1c9c3 | |||
| eefa9648c4 | |||
| 8c26fd2081 | |||
| 49fbb62259 | |||
| ad5a77dc55 | |||
| 2f078326c2 | |||
| 448be9a2cb | |||
| 0a1f675f45 | |||
| 052a50f9b0 | |||
| e331fb4eed | |||
| 4e197f4659 | |||
| 39bf2283cc | |||
| c87d1886cd | |||
| 6f693c4d0a | |||
| 1c5adb96d3 | |||
| 8f8fa8548d | |||
| bbe7575321 | |||
| dae5d054e7 | |||
| 0638cef79b | |||
| 8e2e8c58c9 | |||
| 4b4dde71b3 | |||
| d59b99a2a0 | |||
| df32898523 | |||
| 2bd8e61706 | |||
| 35724f36cd | |||
| 7fba51b59f | |||
| 599d80dcc3 | |||
| 3c186031be | |||
| 51c84df5a5 | |||
| 77c031abc5 | |||
| e81e71c81f | |||
| be07ecec2e | |||
| 3aa0bf11e6 | |||
| 329e68ccb1 | |||
| f3da41ba46 | |||
| 78384b3acf | |||
| 2912d5cd89 | |||
| c699577183 | |||
| 707fe9e0a3 | |||
| 71dd603c3d | |||
| 7aa5f1e4ec | |||
| 7c4e92a030 | |||
| 76125cc83e | |||
| 3fc0fd0bc0 | |||
| 5b0d8f4418 | |||
| d71f377ac1 | |||
| dbd63229c8 | |||
| 1e39cdaba4 | |||
| 5f76fa8d7d | |||
| f6358e319b | |||
| 5a52689d91 | |||
| 5187f0e96c | |||
| 086e471dd5 | |||
| 033de587f2 | |||
| bd0db41f67 | |||
| a5ac79c6a4 | |||
| 67a253164e | |||
| 3c93e0a414 | |||
| 1d9e0faaa3 | |||
| b440c8732f | |||
| 2aae01f3b6 | |||
| c1ffca04a7 | |||
| de703e82cb | |||
| a2a9cdba60 | |||
| 795ce62ce8 | |||
| bde51106a1 | |||
| 3ac9dcf0c0 | |||
| 2d9fe3b0be | |||
| 7e8af9c2ef | |||
| a94ce5d474 | |||
| eceaa6a59b | |||
| 103dd68b3c | |||
| 7decc8e146 | |||
| c58210ba80 | |||
| c68a497e77 | |||
| 03992c1206 | |||
| 7c0ba71314 | |||
| 878a439acd | |||
| f047ef9c55 | |||
| db6ebeb9f2 | |||
| 7631b41b27 | |||
| b2d8ca3023 | |||
| f11ff3475b | |||
| e74cc15d71 | |||
| 0294cb3b98 | |||
| 88dc32a43e | |||
| 42e1277bb9 | |||
| 2dcd95cf2a | |||
| 499abdf26c | |||
| 52e92ac3f9 | |||
| 885ebc8528 | |||
| 4f4ef694ff | |||
| 8cc68989dd | |||
| 57795b3210 | |||
| 68be4dcf57 | |||
| eb4d2a6c7b | |||
| 0c30c2681d | |||
| 36277eaed8 | |||
| ba4cac3fb6 | |||
| 3ce2911e3d | |||
| 89dc3c8805 | |||
| 1bbd6f59fe | |||
| bf6cb764ec | |||
| 7fdde7af3e | |||
| a2a265423f | |||
| 5f39e314b6 | |||
| b7e87b4af8 | |||
| b34b2f3d7b | |||
| 5b6bf42576 | |||
| a72ca7868e | |||
| a0f43c90c4 | |||
| 41b85052f0 | |||
| 4120031e0d | |||
| a499fa19fb | |||
| 6867dabf09 | |||
| 93875818f5 | |||
| d09223c362 | |||
| fb5ae96a44 | |||
| dbe62e82e1 | |||
| e5c72ac13e | |||
| dc143e7cb5 | |||
| 80443892f3 | |||
| 70e89c8f20 | |||
| 4f183f6772 | |||
| f61a7c91e4 | |||
| 610009eb1c | |||
| cd1b783917 | |||
| 57e8ef5463 | |||
| d5581b1bb4 | |||
| 1482ebba64 | |||
| 6d4253dff9 | |||
| cc4c863c82 | |||
| 4618e81d20 | |||
| 7fa971cbcd | |||
| 2267757c41 | |||
| e07727393b | |||
| 6a73930f4b | |||
| 6c382be737 | |||
| c43a234788 | |||
| 4e56321eb9 | |||
| 9e177c73ca | |||
| ccbe1911d2 | |||
| 015a7c10dc | |||
| f0c8eb55a6 | |||
| 24e5b78583 | |||
| ed89858fbc | |||
| a6d5a5f325 | |||
| e566db8396 | |||
| b52cf80bd5 | |||
| f1a31246b2 | |||
| e33b3ceae1 | |||
| c4e78cd3ba | |||
| fa1e74acf2 | |||
| 112a770f2c | |||
| b1bcac5a6d | |||
| ae79229c00 | |||
| 28ac708904 | |||
| 161b446bb2 | |||
| e0aee9863e | |||
| 072484dd5d | |||
| a86d3dc82b | |||
| b141b2edfe | |||
| cf9482bd76 | |||
| 4be2c3b6fd | |||
| 69a75f5c47 | |||
| 0ab4f8513b | |||
| 93f8597dd4 | |||
| 0f0e56a263 | |||
| 91a23bb2d3 | |||
| 19503d92b4 | |||
| e2648bba3e | |||
| 090eaba3aa | |||
| 1f98e75a4f | |||
| a38b4c8ee0 | |||
| 1def205b28 | |||
| 32bee63b50 | |||
| 4798bbdc86 | |||
| a854a341d3 | |||
| 1a4623589c | |||
| 51b19a6831 | |||
| b58ad5f876 | |||
| 4951116a6c | |||
| 46ca49e02b | |||
| 9133d6ecf5 | |||
| ad736edd9e | |||
| 1c7494861d | |||
| 31163e8516 | |||
| 853a26ae8e | |||
| 16c7aaf599 | |||
| d6df2514e1 | |||
| 2b33294a85 | |||
| 3ecdae6ab6 | |||
| fd1c840618 | |||
| 71f8315e06 | |||
| 633f7ffee5 | |||
| 35f53e66dd | |||
| 016d59e81a | |||
| 82851d519d | |||
| cfc18827bb | |||
| 04e8a204a3 | |||
| 4cc7f292b0 | |||
| 7c991b4dfc | |||
| 84d17737cf | |||
| 4c6b32633b | |||
| b933c5cf45 | |||
| 5b76b4a6a3 | |||
| 251d175517 | |||
| 69ce84c8df | |||
| f9e9771a87 | |||
| 18ff819ae9 | |||
| 98c09c653a | |||
| e1d4d85846 | |||
| e52e55a431 | |||
| 32d465dbe8 | |||
| 1f9bc92b66 | |||
| 9e5c82d6f8 | |||
| dd429208ce | |||
| 82278ab115 | |||
| 13cb8d4de7 | |||
| 2bc004f54d | |||
| e58b6041b4 | |||
| 6fd514230c | |||
| f75b4f34d8 | |||
| 6ecf7be826 | |||
| f7d4eebf56 | |||
| 8e735ec453 | |||
| 850bca80ab | |||
| 440dd809e3 | |||
| ffb416e4e8 | |||
| 0a17a9de19 | |||
| a0a7900381 | |||
| 5f77b9faae | |||
| 3fc97c1a83 | |||
| a3a8a7a842 | |||
| e28449b83d | |||
| 6ecc5cdbad | |||
| d64b6fba31 | |||
| d7ac86a6bd | |||
| 598f0d47f8 | |||
| 3b1ccdd2b1 | |||
| 054a8ad85a | |||
| 31ae3ea46e | |||
| 67d453cffe | |||
| ab557d4707 | |||
| e5a21bf7c6 | |||
| d5ef3b7d9b | |||
| f3753c8da0 | |||
| 9fce7ce812 | |||
| 85009ab5b3 | |||
| fe01285d58 | |||
| d1fa4d3439 | |||
| d0921f899c |
@@ -0,0 +1,9 @@
|
||||
---
|
||||
name: Issue
|
||||
about: Submit an Issue to Void
|
||||
title: For VSCode-related issues (eg builds), please start the title with `[App]`. Otherwise, start it with `[Bug]` or `[Feature]`.
|
||||
---
|
||||
|
||||
1. Press `Cmd+Shift+P` in Void, and type `Help: About`. Please paste the information here. Also let us know any other relevant details, like the model and provider you're using if applicable.
|
||||
|
||||
2. Describe the issue/feature here.
|
||||
@@ -1,5 +1,5 @@
|
||||
disturl="https://electronjs.org/headers"
|
||||
target="34.2.0"
|
||||
target="34.3.2"
|
||||
ms_build_id="11044223"
|
||||
runtime="electron"
|
||||
build_from_source="true"
|
||||
|
||||
@@ -12,11 +12,9 @@ There are a few ways to contribute:
|
||||
|
||||
### Codebase Guide
|
||||
|
||||
We [highly recommend reading this](https://github.com/microsoft/vscode/wiki/Source-Code-Organization) article on VSCode's sourcecode organization too. Void's codebase is pretty simple when you know what a service is and what `browser/` and `common/` mean, and the article covers all the jargon.
|
||||
We [highly recommend reading this](https://github.com/voideditor/void/blob/main/VOID_CODEBASE_GUIDE.md) guide that we put together on Void's sourcecode if you'd like to contribute!
|
||||
|
||||
<!-- ADD BLOG HERE
|
||||
We wrote a [guide to working in VSCode].
|
||||
-->
|
||||
The repo is not as intimidating as it first seems if you read the guide!
|
||||
|
||||
Most of Void's code lives in the folder `src/vs/workbench/contrib/void/`.
|
||||
|
||||
@@ -90,7 +88,7 @@ Alternatively, if you want to build Void from the terminal, instead of pressing
|
||||
- If you get `"TypeError: Failed to fetch dynamically imported module"`, make sure all imports end with `.js`.
|
||||
- If you see missing styles, wait a few seconds and then reload.
|
||||
- If you have any questions, feel free to [submit an issue](https://github.com/voideditor/void/issues/new). You can also refer to VSCode's complete [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute) page.
|
||||
|
||||
- If you get errors like `npm error libtool: error: unrecognised option: '-static'`, make sure you have GNU libtool instead of BSD libtool (BSD is the default in macos)
|
||||
|
||||
|
||||
## Packaging
|
||||
@@ -104,12 +102,11 @@ We don't usually recommend packaging. Instead, you should probably just build. I
|
||||
|
||||
### Windows
|
||||
- `npm run gulp vscode-win32-x64` - most common
|
||||
- `npm run gulp vscode-win32-ia32`
|
||||
- `npm run gulp vscode-win32-arm64`
|
||||
|
||||
### Linux
|
||||
- `npm run gulp vscode-linux-x64` - most common
|
||||
- `npm run gulp vscode-linux-arm`
|
||||
- `npm run gulp vscode-linux-ia32`
|
||||
- `npm run gulp vscode-linux-arm64`
|
||||
|
||||
|
||||
### Output
|
||||
@@ -122,7 +119,7 @@ workspace/
|
||||
```
|
||||
|
||||
### Distributing
|
||||
Void's maintainers distribute Void on our website and in releases. If you'd like to see the scripts to convert `Mac .app -> .dmg`, `Windows folder -> .exe`, and `Linux folder -> appimage` for distribution, feel free to reach out.
|
||||
Void's maintainers distribute Void on our website and in releases. Our build pipeline is a fork of VSCodium, and it works by running GitHub Actions which create the downloadables. The build repo with more instructions lives [here](https://github.com/voideditor/void-builder).
|
||||
|
||||
## Pull Request Guidelines
|
||||
|
||||
@@ -15,25 +15,26 @@ This repo contains the full sourcecode for Void. We are currently in [open beta]
|
||||
|
||||
- 👋 [Discord](https://discord.gg/RSNjgaugJs)
|
||||
|
||||
- 🔨 [Contribute](https://github.com/voideditor/void/blob/main/CONTRIBUTING.md)
|
||||
- 🔨 [Contribute](https://github.com/voideditor/void/blob/main/HOW_TO_CONTRIBUTE.md)
|
||||
|
||||
- 🚙 [Roadmap](https://github.com/orgs/voideditor/projects/2)
|
||||
|
||||
- 📝 [Changelog](https://voideditor.com/changelog)
|
||||
|
||||
- 🧭 [Codebase Guide](https://github.com/voideditor/void/blob/main/VOID_CODEBASE_GUIDE.md)
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Feel free to attend a weekly meeting in our Discord channel if you'd like to contribute!
|
||||
|
||||
2. To get started working on Void, see [Contributing](https://github.com/voideditor/void/blob/main/CONTRIBUTING.md).
|
||||
2. To get started working on Void, see [`HOW_TO_CONTRIBUTE`](https://github.com/voideditor/void/blob/main/HOW_TO_CONTRIBUTE.md).
|
||||
|
||||
3. We're open to collaborations and suggestions of all types - just reach out.
|
||||
|
||||
|
||||
## Reference
|
||||
|
||||
Void is a fork of the [vscode](https://github.com/microsoft/vscode) repository. For some useful links on VSCode, see [`VOID_USEFUL_LINKS.md`](https://github.com/voideditor/void/blob/main/VOID_USEFUL_LINKS.md).
|
||||
Void is a fork of the [vscode](https://github.com/microsoft/vscode) repository. For a guide to the VSCode/Void codebase, see [`VOID_CODEBASE_GUIDE`](https://github.com/voideditor/void/blob/main/VOID_CODEBASE_GUIDE.md).
|
||||
|
||||
## Support
|
||||
Feel free to reach out in our Discord or contact us via email: hello@voideditor.com.
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
# Void Codebase Guide
|
||||
|
||||
The Void codebase is not as intimidating as it seems!
|
||||
|
||||
Most of Void's code lives in the folder `src/vs/workbench/contrib/void/`.
|
||||
|
||||
The purpose of this document is to explain how Void's codebase works. If you want build instructions instead, see [Contributing](https://github.com/voideditor/void/blob/main/HOW_TO_CONTRIBUTE.md).
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Void Codebase Guide
|
||||
|
||||
### VSCode Rundown
|
||||
Here's a VSCode rundown if you're just getting started with Void. You can also see Microsoft's [wiki](https://github.com/microsoft/vscode/wiki/Source-Code-Organization) for some pictures. VSCode is an Electron app. Electron runs two processes: a **main** process (for internals) and a **browser** process (browser means HTML in general, not just "web browser").
|
||||
<p align="center" >
|
||||
<img src="https://github.com/user-attachments/assets/eef80306-2bfe-4cac-ba15-6156f65ab3bb" alt="Credit - https://github.com/microsoft/vscode/wiki/Source-Code-Organization" width="700px">
|
||||
</p>
|
||||
|
||||
- Code in a `browser/` folder always lives on the browser process, and it can use `window` and other browser items.
|
||||
- Code in an `electron-main/` folder always lives on the main process, and it can import `node_modules`.
|
||||
- Code in `common/` can be used by either process, but doesn't get any special imports.
|
||||
- The browser environment is not allowed to import `node_modules`. We came up with two workarounds:
|
||||
1. Bundle the raw node_module code to the browser - we're doing this for React.
|
||||
2. Implement the code on `electron-main/` and set up a channel between main/browser - we're doing this for sendLLMMessage.
|
||||
|
||||
|
||||
|
||||
|
||||
### Terminology
|
||||
|
||||
Here's some terminology you might want to know about when working inside VSCode:
|
||||
- An **Editor** is the thing that you type your code in. If you have 10 tabs open, that's just one editor! Editors contain tabs (or "models").
|
||||
- A **Model** is an internal representation of a file's contents. It's shared between editors (for example, if you press `Cmd+\` to make a new editor, then the model of a file like `A.ts` is shared between them. Two editors, one model. That's how changes sync.).
|
||||
- Each model has a **URI** it represents, like `/Users/.../my_file.txt`. (A URI or "resource" is generally just a path).
|
||||
- The **Workbench** is the wrapper that contains all the editors, the terminal, the file system tree, etc.
|
||||
- Usually you use the `ITextModel` type for models and the `ICodeEditor` type for editors. There aren't that many other types.
|
||||
<p align="center" >
|
||||
<img src="https://github.com/user-attachments/assets/6521c228-dc96-4cf5-a673-6b9ca78b9b06" alt="Credit - https://code.visualstudio.com/docs/getstarted/userinterface" width="400px">
|
||||
</p>
|
||||
|
||||
|
||||
|
||||
- VSCode is organized into "**Services**". A service is just a class that mounts a single time (in computer science theory this is called a "singleton"). You can register services with `registerSingleton` so that you can easily use them in any constructor with `@<Service>`. See _dummyContrib for an example we put together on how to register them. The registration is the same every time.
|
||||
|
||||
- "**Actions**" are functions you register on VSCode so that either you or the user can call them later. They're also called "**Commands**".
|
||||
- You can run actions as a user by pressing Cmd+Shift+P (opens the command pallete), or you can run them internally by using the commandService to call them by ID. We use actions to register keybinding listeners like Cmd+L, Cmd+K, etc. The nice thing about actions is the user can change the keybindings.
|
||||
|
||||
|
||||
### Internal LLM Message Pipeline
|
||||
|
||||
Here's a picture of all the dependencies that are relevent between the time you first send a message through Void's sidebar, and the time a request is sent to your provider.
|
||||
Sending LLM messages from the main process avoids CSP issues with local providers and lets us use node_modules more easily.
|
||||
|
||||
|
||||
<div align="center">
|
||||
<img width="100%" src="https://github.com/user-attachments/assets/9cf54dbb-82c4-4488-97a2-bd8dea890b50">
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
**Notes:** `modelCapabilities` is an important file that must be updated when new models come out!
|
||||
|
||||
|
||||
### Apply
|
||||
|
||||
Void has two types of Apply: **Fast Apply** (uses Search/Replace, see below), and **Slow Apply** (rewrites whole file).
|
||||
|
||||
When you click Apply and Fast Apply is enabled, we prompt the LLM to output Search/Replace block(s) like this:
|
||||
```
|
||||
<<<<<<< ORIGINAL
|
||||
// original code goes here
|
||||
=======
|
||||
// replaced code goes here
|
||||
>>>>>>> UPDATED
|
||||
```
|
||||
This is what allows Void to quickly apply code even on 1000-line files. It's the same as asking the LLM to press Ctrl+F and enter in a search/replace query.
|
||||
|
||||
### Apply Inner Workings
|
||||
|
||||
The `editCodeService` file runs Apply. The same exact code is also used when the LLM calls the Edit tool, and when you submit Cmd+K. Just different versions of Fast/Slow Apply mode.
|
||||
|
||||
Here is some important terminology:
|
||||
- A **DiffZone** is a {startLine, endLine} region of text where we compute and show red/green areas, or **Diffs**. When any changes are made to a file, we loop through all the DiffAreas on that file and refresh its Diffs.
|
||||
- A **DiffArea** is a generalization that just tracks line numbers like a DiffZone.
|
||||
- The only type of DiffArea that can "stream" is a DiffZone. Each DiffZone has an llmCancelToken if it's streaming.
|
||||
|
||||
How Apply works:
|
||||
- When you click Apply, we create a **DiffZone** over that the full file so that any changes that the LLM makes will show up in red/green. We then stream the change.
|
||||
- When an LLM calls Edit, it's really calling Apply.
|
||||
- When you submit Cmd+K, it's the same as Apply except we create a smaller DiffZone (not on the whole file).
|
||||
|
||||
|
||||
### Writing Files Inner Workings
|
||||
When Void wants to change your code, it just writes to a text model. This means all you need to know to write to a file is its URI - you don't have to load it, save it, etc. There are some annoying background URI/model things to think about to get this to work, but we handled them all in `voidModelService`.
|
||||
|
||||
### Void Settings Inner Workings
|
||||
We have a service `voidSettingsService` that stores all your Void settings (providers, models, global Void settings, etc). Imagine this as an implicit dependency for any of the core Void services:
|
||||
|
||||
<div align="center">
|
||||
<img width="800" src="https://github.com/user-attachments/assets/9f3cb68c-a61b-4810-8429-bb90b992b3fa">
|
||||
</div>
|
||||
|
||||
Here's a guide to some of the terminology we're using:
|
||||
- **FeatureName**: Autocomplete | Chat | CtrlK | Apply
|
||||
- **ModelSelection**: a {providerName, modelName} pair.
|
||||
- **ProviderName**: The name of a provider: `'ollama'`, `'openAI'`, etc.
|
||||
- **ModelName**: The name of a model (string type, eg `'gpt-4o'`).
|
||||
- **RefreshProvider**: a provider that we ping repeatedly to update the models list.
|
||||
- **ChatMode** = normal | gather | agent
|
||||
|
||||
|
||||
|
||||
### Approval State
|
||||
`editCodeService`'s data structures contain all the information about changes that the user needs to review. However, they don't store that information in a useful format. We wrote the following service to get a more useful derived state:
|
||||
|
||||
<div align="center">
|
||||
<img width="600" src="https://github.com/user-attachments/assets/f3645355-dff6-467c-bc38-ffe52077c08b">
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
### Build process
|
||||
If you want to know how our build pipeline works, see our build repo [here](https://github.com/voideditor/void-builder).
|
||||
|
||||
|
||||
|
||||
## VSCode Codebase Guide
|
||||
|
||||
The Void team put together this list of links to get up and running with VSCode's sourcecode, the foundation of Void. We hope it's helpful!
|
||||
|
||||
#### Links for Beginners
|
||||
|
||||
- [VSCode UI guide](https://code.visualstudio.com/docs/getstarted/userinterface) - covers auxbar, panels, etc.
|
||||
|
||||
- [UX guide](https://code.visualstudio.com/api/ux-guidelines/overview) - covers Containers, Views, Items, etc.
|
||||
|
||||
#### Links for Contributors
|
||||
|
||||
- [How VSCode's sourcecode is organized](https://github.com/microsoft/vscode/wiki/Source-Code-Organization) - this explains where the entry point files are, what `browser/` and `common/` mean, etc. This is the most important read on this whole list! We recommend reading the whole thing.
|
||||
|
||||
- [Built-in VSCode styles](https://code.visualstudio.com/api/references/theme-color) - CSS variables that are built into VSCode. Use `var(--vscode-{theme but replacing . with -})`. You can also see their [Webview theming guide](https://code.visualstudio.com/api/extension-guides/webview#theming-webview-content).
|
||||
|
||||
|
||||
#### Misc
|
||||
|
||||
- [Every command](https://code.visualstudio.com/api/references/commands) built-in to VSCode - not used often, but here for reference.
|
||||
|
||||
- Note: VSCode's repo is the source code for the Monaco editor! An "editor" is a Monaco editor, and it shares the code for ITextModel, etc.
|
||||
|
||||
|
||||
#### VSCode's Extension API
|
||||
|
||||
Void is no longer an extension, so these links are no longer required, but they might be useful if we ever build an extension again.
|
||||
|
||||
- [Files you need in an extension](https://code.visualstudio.com/api/get-started/extension-anatomy).
|
||||
|
||||
- [An extension's `package.json` schema](https://code.visualstudio.com/api/references/extension-manifest).
|
||||
|
||||
- ["Contributes" Guide](https://code.visualstudio.com/api/references/contribution-points) - the `"contributes"` part of `package.json` is how an extension mounts.
|
||||
|
||||
- [The Full VSCode Extension API](https://code.visualstudio.com/api/references/vscode-api) - look on the right side for organization. The [bottom](https://code.visualstudio.com/api/references/vscode-api#api-patterns) of the page is easy to miss but is useful - cancellation tokens, events, disposables.
|
||||
|
||||
- [Activation events](https://code.visualstudio.com/api/references/activation-events) you can define in `package.json` (not the most useful).
|
||||
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
# Useful links
|
||||
|
||||
The Void team put together this list of links to get up and running with VSCode's sourcecode. We hope it's helpful!
|
||||
|
||||
For a complete guide on building Void, see [Contributing](https://github.com/voideditor/void/blob/main/CONTRIBUTING.md).
|
||||
|
||||
## Contributing
|
||||
|
||||
- [How VSCode's sourcecode is organized](https://github.com/microsoft/vscode/wiki/Source-Code-Organization) - this explains where the entry point files are, what `browser/` and `common/` mean, etc. This is the most important read on this whole list! We recommend reading the whole thing.
|
||||
|
||||
- [Built-in VSCode styles](https://code.visualstudio.com/api/references/theme-color) - CSS variables that are built into VSCode. Use `var(--vscode-{theme but replacing . with -})`. You can also see their [Webview theming guide](https://code.visualstudio.com/api/extension-guides/webview#theming-webview-content).
|
||||
|
||||
## Beginners / Getting started
|
||||
|
||||
- [VSCode UI guide](https://code.visualstudio.com/docs/getstarted/userinterface) - covers auxbar, panels, etc.
|
||||
|
||||
- [UX guide](https://code.visualstudio.com/api/ux-guidelines/overview) - covers Containers, Views, Items, etc.
|
||||
|
||||
|
||||
## Misc
|
||||
|
||||
- [Every command](https://code.visualstudio.com/api/references/commands) built-in to VSCode - not used often, but here for reference.
|
||||
|
||||
|
||||
## VSCode's Extension API
|
||||
|
||||
Void is no longer an extension, so these links are no longer required, but they might be useful if we ever build an extension again.
|
||||
|
||||
- [Files you need in an extension](https://code.visualstudio.com/api/get-started/extension-anatomy).
|
||||
|
||||
- [An extension's `package.json` schema](https://code.visualstudio.com/api/references/extension-manifest).
|
||||
|
||||
- ["Contributes" Guide](https://code.visualstudio.com/api/references/contribution-points) - the `"contributes"` part of `package.json` is how an extension mounts.
|
||||
|
||||
- [The Full VSCode Extension API](https://code.visualstudio.com/api/references/vscode-api) - look on the right side for organization. The [bottom](https://code.visualstudio.com/api/references/vscode-api#api-patterns) of the page is easy to miss but is useful - cancellation tokens, events, disposables.
|
||||
|
||||
- [Activation events](https://code.visualstudio.com/api/references/activation-events) you can define in `package.json` (not the most useful).
|
||||
|
||||
|
||||
@@ -1,75 +1,75 @@
|
||||
c0a187acca68906c4a6387e8fabd052cb031ace6132d60a71001d9a0e891958e *chromedriver-v34.2.0-darwin-arm64.zip
|
||||
fa5a46d752267d8497d375e19079e8b6a8df70c234a79b2d6b48f5862e1a0abc *chromedriver-v34.2.0-darwin-x64.zip
|
||||
61e03d4fa570976d80f740637f56192b6448a05a73d1fba9717900b29f2b1b4d *chromedriver-v34.2.0-linux-arm64.zip
|
||||
ec774d9b1a1b828a0db1502a1017fcab1dfed99b1b6b2fd2308dd600a1efa98a *chromedriver-v34.2.0-linux-armv7l.zip
|
||||
cc15a6e6206485a2d96649ceb60509b9da04fa2811c4824b2e0eb43d1f4b1417 *chromedriver-v34.2.0-linux-x64.zip
|
||||
9777122f6684180ef375b9b21dcabbc731d8a8befa300d1d47ad954a5b64c1c8 *chromedriver-v34.2.0-mas-arm64.zip
|
||||
69451fa148b105fec9644646b22ca758a206499574c5816591354835c8056679 *chromedriver-v34.2.0-mas-x64.zip
|
||||
eb7adc7e720f5e0f1d2c12ecbe886bdc01f2c9aaa3954bd6ebd313750bb18819 *chromedriver-v34.2.0-win32-arm64.zip
|
||||
cb1973b0c2f5565974d5c2cb51816692f064b6cdc7897fa341d528ba7f9b14bf *chromedriver-v34.2.0-win32-ia32.zip
|
||||
af5575b4727c3dbe7272016cbbaa8872043f843168a47d86748a50397efb4f77 *chromedriver-v34.2.0-win32-x64.zip
|
||||
fcc718af2a28fb953290dc971e945818b4dbb293f297e6e25acb669d450cc0dd *electron-api.json
|
||||
fa47e752e559a6472b87d8907f63296ed8cd53ecf862a4ae113018474d40aa8e *electron-v34.2.0-darwin-arm64-dsym-snapshot.zip
|
||||
336c3374e721e2379901141be6345459f78d243b037c65b55bac4ae8cb14bfbe *electron-v34.2.0-darwin-arm64-dsym.zip
|
||||
ac3b9d712d9f036f066d8eba42797117a513e2d250fcc117f0354300b7d5c1e5 *electron-v34.2.0-darwin-arm64-symbols.zip
|
||||
ee447c17b2ac545e48083113d7e39a916821c1316f60f42cbcbee4fffe7c022a *electron-v34.2.0-darwin-arm64.zip
|
||||
d7510bc038d06b26690ac9a78fbb256626502303ff7f5b1c2242f64a02832b1b *electron-v34.2.0-darwin-x64-dsym-snapshot.zip
|
||||
0e95c2bbda00afe78e6229b824e3ffe0cc8612956dc11a5a30380224acdbecae *electron-v34.2.0-darwin-x64-dsym.zip
|
||||
6d734ef8e8fd007071aae9a13534894dd801f11900f3e73e49cf5352b426e575 *electron-v34.2.0-darwin-x64-symbols.zip
|
||||
8ef741819c8a5370dabc3b9df5e6ac217366477c5d5c656ed23c800bc984d887 *electron-v34.2.0-darwin-x64.zip
|
||||
c2f448882a0392ebfd9810058a6a9580b087002c74fca3dcd3cf4ba5c3b27a7c *electron-v34.2.0-linux-arm64-debug.zip
|
||||
51e887c382593021127593ceba89ad662d3a6de0f748b94fe3a0ce78a7393923 *electron-v34.2.0-linux-arm64-symbols.zip
|
||||
818c91309da8ff948c43df58a996c05c0d27daa690e1d659355d9db01e351919 *electron-v34.2.0-linux-arm64.zip
|
||||
c2f448882a0392ebfd9810058a6a9580b087002c74fca3dcd3cf4ba5c3b27a7c *electron-v34.2.0-linux-armv7l-debug.zip
|
||||
71ec2b7473db766194bcf04648229c4affedce06e150e692745cc72dbc3749c5 *electron-v34.2.0-linux-armv7l-symbols.zip
|
||||
0c75996c6bf2d37d0441d3e1021386a9348f8d21783d75571cdb10ede606424f *electron-v34.2.0-linux-armv7l.zip
|
||||
d1f17be8df8ec6dc1a23afd8a7752f0b949c755a493b8a2c1e83f76c629258ca *electron-v34.2.0-linux-x64-debug.zip
|
||||
d8fa79154b0b663dbd0054d69f53aec326997449ef21943d27225a2d6899660f *electron-v34.2.0-linux-x64-symbols.zip
|
||||
f12a02d86cc657500978d263ec6d1841b6e2085cd3bd4d30a97cfe14685396f3 *electron-v34.2.0-linux-x64.zip
|
||||
71ef8bfebb8513a405fd2beb44ad18f802bbac2248f81a5328ddaaa12906d70c *electron-v34.2.0-mas-arm64-dsym-snapshot.zip
|
||||
437aca6cad3158f15fd852e5913462637052940f812b20ccc10fccc133d5a0d6 *electron-v34.2.0-mas-arm64-dsym.zip
|
||||
7f253375a7b43d34b770c03153e47185be7a64bc0c10a783993a93144eb35492 *electron-v34.2.0-mas-arm64-symbols.zip
|
||||
1f601c20430b036b485c7dc02a39a297f0d08905462a4568306c12f299375e2f *electron-v34.2.0-mas-arm64.zip
|
||||
d32664181804a17f825bf1fbfd8f0cbe01e0b41b284937359e6d9754b3481ed8 *electron-v34.2.0-mas-x64-dsym-snapshot.zip
|
||||
f6b394b89fb77dfeefdf525739fe8cd9695f8c2ac2251ed7c571a376cde059e9 *electron-v34.2.0-mas-x64-dsym.zip
|
||||
539328d93e9bc122e6e34faeab6a42f4845c523f796c7a11bbdcfe5e856f6b98 *electron-v34.2.0-mas-x64-symbols.zip
|
||||
749b6ced7a9d35a719ad98d4c3bf1a08c315c19ce97d2497235d4ac302cda665 *electron-v34.2.0-mas-x64.zip
|
||||
e843ea4cb7a93686728d056c957c124760886167932ff619b518e45917edf38a *electron-v34.2.0-win32-arm64-pdb.zip
|
||||
4908423be5f8ad1b5dd737edfd69ee0870460e16bb639bb963b0981bda2628e4 *electron-v34.2.0-win32-arm64-symbols.zip
|
||||
9d13b2bd61416eec28f43faa399cc5c0dc9e36dec226314bbf397828f55d74de *electron-v34.2.0-win32-arm64-toolchain-profile.zip
|
||||
1629cec7b5620e6ca3b5305c393ae147d1a3871a8f164d686b7bee3810fc1109 *electron-v34.2.0-win32-arm64.zip
|
||||
e7182f1ef5ed13187fe12f35133cefb50c59e1d52ada758cbb72813dda575205 *electron-v34.2.0-win32-ia32-pdb.zip
|
||||
a10f778f62bf060a7e38f5ea75ea472679b9ef4f12767ee0703e6d77effae78a *electron-v34.2.0-win32-ia32-symbols.zip
|
||||
9d13b2bd61416eec28f43faa399cc5c0dc9e36dec226314bbf397828f55d74de *electron-v34.2.0-win32-ia32-toolchain-profile.zip
|
||||
96396712a0240f04471f71246b3885a513b08e3f2d40154272d34e59419db7e6 *electron-v34.2.0-win32-ia32.zip
|
||||
0c55ef5b1a6ea4604e3f0e9f8b5e946944abd1cfc3b974432d40e924a9996812 *electron-v34.2.0-win32-x64-pdb.zip
|
||||
d6b67cf12edabcc62ae21e7c90ac6b1161dbefe4e6b765c69fc7040096b7d026 *electron-v34.2.0-win32-x64-symbols.zip
|
||||
9d13b2bd61416eec28f43faa399cc5c0dc9e36dec226314bbf397828f55d74de *electron-v34.2.0-win32-x64-toolchain-profile.zip
|
||||
a4fdf617dca787b7f1c6f8d0d1cb69c8adb37ee23f9553fe69803f9cad713360 *electron-v34.2.0-win32-x64.zip
|
||||
5ca44a4ee9cc74a56c9556ce3f3774986dbb8b4f4953088c7f4577bfba4356a8 *electron.d.ts
|
||||
86247a6815cc98f321374edcbfc0ab7ee79bcb13a5c25213f42151e4059ec690 *ffmpeg-v34.2.0-darwin-arm64.zip
|
||||
e94f4707a91194f97d0451f9d5a27982c873a5c13d83d20926ce6fdb613f536c *ffmpeg-v34.2.0-darwin-x64.zip
|
||||
947d7b7cb035eab94cc15b602dfa8a925cf238c1d9225c01732fe0c41f59c571 *ffmpeg-v34.2.0-linux-arm64.zip
|
||||
64d74b6b94ac4fd755b97e0ec8d50af9cd73810fdb52f6c081b13a3337ace0ea *ffmpeg-v34.2.0-linux-armv7l.zip
|
||||
a62cb20c5e5f2ba6f1df6f1bc406cc30f7ed44fe6380b506afa333d1b4ad7a87 *ffmpeg-v34.2.0-linux-x64.zip
|
||||
715bbb3d193b1ca57d4b329edd05cd6b125dc188cdb73d1c66f220f62c8562e3 *ffmpeg-v34.2.0-mas-arm64.zip
|
||||
866bf47106e8c3e50f046783eb8f5c756069c6de962a46f4edc7a5ee7e4593ee *ffmpeg-v34.2.0-mas-x64.zip
|
||||
71a2a6c4cca15ccbcbf8912f5d73f855b0ca79804f941f68527ae808f8163957 *ffmpeg-v34.2.0-win32-arm64.zip
|
||||
399f841cca166781078ca42c8ea060e3d5850ec6cb79716186d0806f3ce20842 *ffmpeg-v34.2.0-win32-ia32.zip
|
||||
afad2a44f20a0c0c01fb1fea637f3f821842399593c9961c74d650ca12d32cbb *ffmpeg-v34.2.0-win32-x64.zip
|
||||
c95fdc9dba05aa68aeccb69d4c34f0cb1fb98d7f5291d974d0b638488693655f *hunspell_dictionaries.zip
|
||||
0abe74138afdb6e45a085d77407659f13c75ab96f694313d4e98bd662f9c6df2 *libcxx-objects-v34.2.0-linux-arm64.zip
|
||||
3d0cbf6fb150b006428eab78235856d2204d5e93ca85f162e429b4c8bd9b0d3b *libcxx-objects-v34.2.0-linux-armv7l.zip
|
||||
3a5491e32cec825499919be1b8bf0550d28fe5a31ff00a95572d49a58bb4820a *libcxx-objects-v34.2.0-linux-x64.zip
|
||||
ff8753d52f759041b8e5462125ee2b96798fe8b5047f8fb8ae60cd07af2fb13d *libcxx_headers.zip
|
||||
c98cce0091681bc367a48f66c5f4602961aa9cb6dd1a995d8969d6b39ce732f3 *libcxxabi_headers.zip
|
||||
78a9606190fb227460ddcd276ad540873595d6a82fd1007f42900f53347e3eb1 *mksnapshot-v34.2.0-darwin-arm64.zip
|
||||
6d4587a36f509356a908b6752de527cfe8a294a2d435b82ea3a98288c3a3a178 *mksnapshot-v34.2.0-darwin-x64.zip
|
||||
17737bd34f7feefc5143b745f2c4f0e5a41678780e144eb43de41f8d4b9d7852 *mksnapshot-v34.2.0-linux-arm64-x64.zip
|
||||
565aa9a84d913b7b5eb8d3b868cff151cb8a6c16548233ffd92b2f9bf3789227 *mksnapshot-v34.2.0-linux-armv7l-x64.zip
|
||||
7332d7864ab4e5ee7fa8d00580d9357f30d0e69d3d1434d67c61f79f666314a4 *mksnapshot-v34.2.0-linux-x64.zip
|
||||
cc81bfc9894378a9fc8a777429e04cc80860663b3dff8eba1f8cc72559a4f23d *mksnapshot-v34.2.0-mas-arm64.zip
|
||||
c98230088698638159f6e7e0c5ddde3cb4dba9fa81d76e3c58fc86f96b63ef81 *mksnapshot-v34.2.0-mas-x64.zip
|
||||
cc7b42943d998e1083ad8119dc2201dd27d709c15aa2a9b78f409485d2927592 *mksnapshot-v34.2.0-win32-arm64-x64.zip
|
||||
ade9e3126f113318e226f9bdeba068b4383b30a98517a361386eff448459effa *mksnapshot-v34.2.0-win32-ia32.zip
|
||||
344292ea318dc0e21f37bc7c82d57a57449f0fb278d9868f1b1b597bdcb1f36f *mksnapshot-v34.2.0-win32-x64.zip
|
||||
c9b82c9f381742e839fea00aeb14f24519bcaf38a0f4eed25532191701f9535b *chromedriver-v34.3.2-darwin-arm64.zip
|
||||
d556c1e2b06f1bf131e83c2fb981de755c28e1083a884d257eb964815be16b0c *chromedriver-v34.3.2-darwin-x64.zip
|
||||
1cabad4f3303ac2ff172a9f22185f64944dbaa6fc68271609077158eaefdee35 *chromedriver-v34.3.2-linux-arm64.zip
|
||||
4213ce52c72ef414179b5c5c22ae8423847ff030d438296bd6c2aac763930a7b *chromedriver-v34.3.2-linux-armv7l.zip
|
||||
3c64c08221fdfc0f4be60ea8b1b126f2ecca45f60001b63778522f711022c6ea *chromedriver-v34.3.2-linux-x64.zip
|
||||
e8388734d88e011cb6cd79795431de9206820749219d80565ee49d90501d2bf3 *chromedriver-v34.3.2-mas-arm64.zip
|
||||
3ad1dd37bd6e0bb37e8503898db7aedd56bd5213e6d6760b05c3d11f4625062b *chromedriver-v34.3.2-mas-x64.zip
|
||||
d567b481a0f5d88e84bba7718f89fb08f56363bfc4cb5914e1c2086358a5c252 *chromedriver-v34.3.2-win32-arm64.zip
|
||||
df6732e9dc61cb20a3c0b2a2de453aac7e2bd54e7cbff43512afa614852c15fa *chromedriver-v34.3.2-win32-ia32.zip
|
||||
dda0765c8d064924632e18cd152014ecd767f3808fc51c8249a053bfb7ca70a2 *chromedriver-v34.3.2-win32-x64.zip
|
||||
1945f15caff98f2e0f1ee539c483d352fb8d4d0c13f342caa7abe247676d828c *electron-api.json
|
||||
c078bbf727b3c3026f60e07a0f4643b85c06c581b54be017d0a6c284ba6772d3 *electron-v34.3.2-darwin-arm64-dsym-snapshot.zip
|
||||
35f587754d6a3272606258386bf73688d63dd53c7e572d3a7cbaae6f3f60bdae *electron-v34.3.2-darwin-arm64-dsym.zip
|
||||
08b14ee02c98353de3c738120dfd017322666e82b914a7f6de9b9888dcc5c0f0 *electron-v34.3.2-darwin-arm64-symbols.zip
|
||||
2a4aa7e8fa30f229e465ebd18d3e4722e2b41529dc51a68a954d333a7e556ffe *electron-v34.3.2-darwin-arm64.zip
|
||||
1509ccdeb80024f5e3edd5ecf804b4cef4e47ea2bd74e33ef0b39044b0ccf892 *electron-v34.3.2-darwin-x64-dsym-snapshot.zip
|
||||
3bbe5d587c3f582ed8c126b0fb635cc02ad9a14d077b04892fe6f862092445b0 *electron-v34.3.2-darwin-x64-dsym.zip
|
||||
fa7ece82e6ecaf1c94ed341e8ebff98e64687c68fe113f52cd9a21400302e22f *electron-v34.3.2-darwin-x64-symbols.zip
|
||||
23938c62257a65a863ed7aa7c7966ba5f257a7d3dc16c78293e826962cc39c5c *electron-v34.3.2-darwin-x64.zip
|
||||
0547eecf8ab538d74fa854f591ce8b888a3dbb339256d2db3038e7bb2c6dd929 *electron-v34.3.2-linux-arm64-debug.zip
|
||||
676d0dc2b1c1c85c8b2abbb8cd5376ee22ecdb910493b910d9ae5a998532136a *electron-v34.3.2-linux-arm64-symbols.zip
|
||||
774e4ccb39d553e5487994a9f8c60774a90f08cdb049ff65f3963fc27c969ff2 *electron-v34.3.2-linux-arm64.zip
|
||||
0547eecf8ab538d74fa854f591ce8b888a3dbb339256d2db3038e7bb2c6dd929 *electron-v34.3.2-linux-armv7l-debug.zip
|
||||
ba33bf53fcb35dea568a2795f5b23ecf46c218abe8258946611c72a1f42f716c *electron-v34.3.2-linux-armv7l-symbols.zip
|
||||
73ae92c8fffb351d3a455569cf57ce9a3f676f42bf61939c613c607fe0fc3bfb *electron-v34.3.2-linux-armv7l.zip
|
||||
e61a9a69dd7ea6f2687708a8e83516670cdea53c728226e598e2f6f1fad5b77b *electron-v34.3.2-linux-x64-debug.zip
|
||||
f1a04df7fe67dd1cd29e7b87871525458d2eb24c0cf3b5835a1c56974707562a *electron-v34.3.2-linux-x64-symbols.zip
|
||||
7b74c0c4fae82e27c7e9cbca13e9763e046113dba8737d3e27de9a0b300ac87e *electron-v34.3.2-linux-x64.zip
|
||||
8571a6aa83e00925ceb39fdc5a45a9f6b9aa3d92fd84951c6f252ed723aea4ae *electron-v34.3.2-mas-arm64-dsym-snapshot.zip
|
||||
477410c6f9a6c5eeaedf376058a02c2996fc0a334aa40eeec7d3734c09347f4d *electron-v34.3.2-mas-arm64-dsym.zip
|
||||
c2e62dcd6630cb51b2d8e2869e74e47d29bda785521cea6e82e546d0fc58aabb *electron-v34.3.2-mas-arm64-symbols.zip
|
||||
a1698e8546a062fd59b7f8e5507a7f3220fb00b347f2377de83fc9a07f7f3507 *electron-v34.3.2-mas-arm64.zip
|
||||
741a24ac230a3651dca81d211f9f00b835c428a5ed0c5f67d370d4e88b62f8d6 *electron-v34.3.2-mas-x64-dsym-snapshot.zip
|
||||
aeff97ec9e5c9e173ac89e38acd94476025c5640d5f27be1e8c2abd54398bab3 *electron-v34.3.2-mas-x64-dsym.zip
|
||||
9f14b66b1d612ac66697288e8763171c388f7f200854871a5f0ab464a6a921c2 *electron-v34.3.2-mas-x64-symbols.zip
|
||||
c979d7e7175f1e8e03ca187997d4c156b878189fc3611b347fadebcb16f3e027 *electron-v34.3.2-mas-x64.zip
|
||||
f43c700641e8220205dd356952e32718d113cf530520c4ed7209b59851eac266 *electron-v34.3.2-win32-arm64-pdb.zip
|
||||
3ba6e01c99bffac6b5dd3fd6f122ecdb571cf6f675dc5498c65050bd7a382ef8 *electron-v34.3.2-win32-arm64-symbols.zip
|
||||
c23f84aabb09c24cd2ae759a547fdba4206af19a3bb0f4554a91cd9528648ad0 *electron-v34.3.2-win32-arm64-toolchain-profile.zip
|
||||
9b9cb65d75a16782088b492f9ef3bb4d27525012b819c12bf29bd27e159d749b *electron-v34.3.2-win32-arm64.zip
|
||||
1006e7af4c149114b5ebc3497617aaa6cd1bb0b131e0a225fd73709ff308f9c5 *electron-v34.3.2-win32-ia32-pdb.zip
|
||||
1ecb6430cd04454f08f557c9579163f3552144bfcc0b67b768dad8868b5b891d *electron-v34.3.2-win32-ia32-symbols.zip
|
||||
c23f84aabb09c24cd2ae759a547fdba4206af19a3bb0f4554a91cd9528648ad0 *electron-v34.3.2-win32-ia32-toolchain-profile.zip
|
||||
d004fd5f853754001fafaec33e383d1950b30c935ee71b297ec1c9e084355e9b *electron-v34.3.2-win32-ia32.zip
|
||||
4e0721552fd2f09e9466e88089af8b965f1bfbc4ae00a59aaf6245b1d1efabfd *electron-v34.3.2-win32-x64-pdb.zip
|
||||
9dea812a7e7cd0fb18e5fed9a99db5531959a068c24d3c0ecedceb644cd3ffa0 *electron-v34.3.2-win32-x64-symbols.zip
|
||||
c23f84aabb09c24cd2ae759a547fdba4206af19a3bb0f4554a91cd9528648ad0 *electron-v34.3.2-win32-x64-toolchain-profile.zip
|
||||
1785e161420fb90d2331c26e50bba3413cae9625b7db3c8524ea02ade631efba *electron-v34.3.2-win32-x64.zip
|
||||
722b304c31ddac58b0083d94a241c5276464f04bd8ea4fcbfd33051d197be103 *electron.d.ts
|
||||
31ce159b2e47d1de5bc907d8e1c89726b0f2ba530ec2e9d7a8e5c723b1ccf6e0 *ffmpeg-v34.3.2-darwin-arm64.zip
|
||||
565539bac64a6ee9cf6f188070f520210a1507341718f5dc388ac7c454b1e1d5 *ffmpeg-v34.3.2-darwin-x64.zip
|
||||
6006ea0f46ab229feb2685be086b0fafd65981e2939dd2218a078459c75ab527 *ffmpeg-v34.3.2-linux-arm64.zip
|
||||
9404ce2e85df7c40f809f2cf62c7af607de299839fe6b7ae978c3015500abcc8 *ffmpeg-v34.3.2-linux-armv7l.zip
|
||||
79aec96898b7e2462826780ee0b52b9ab299dc662af333e128a34fd5ddae87f1 *ffmpeg-v34.3.2-linux-x64.zip
|
||||
9190743c78210574faf5d5ecb82a00f8fa15e5f2253378cb925a99ca9d39961b *ffmpeg-v34.3.2-mas-arm64.zip
|
||||
48915adcb1a6342efeda896035101300f0432c0304cfb38f2378e98c6309ebae *ffmpeg-v34.3.2-mas-x64.zip
|
||||
745d5ef786de6d4a720475079836e2fda7b501cfcd255819485a47de5b24b74e *ffmpeg-v34.3.2-win32-arm64.zip
|
||||
d0d86d60978439dc8ae4a723d4e4c1f853891d596bfd84033440a232fa762e2f *ffmpeg-v34.3.2-win32-ia32.zip
|
||||
4441539fd8c9cbe79880ff1bade9bdc0c3744c33d7409130af6404e57ee401ff *ffmpeg-v34.3.2-win32-x64.zip
|
||||
39edd1eeefe881aa75af0e438204e0b1c6e6724e34fa5819109276331c0c2c9a *hunspell_dictionaries.zip
|
||||
20dd417536e5f4ebc01f480221284c0673729c27b082bc04e2922f16cd571719 *libcxx-objects-v34.3.2-linux-arm64.zip
|
||||
7e53c5779c04f895f8282c0450ec4a63074d15a0e910e41006cfea133d0288af *libcxx-objects-v34.3.2-linux-armv7l.zip
|
||||
92e2283c924ab523ffec3ea22513beaab6417f7fc3d570f57d51a1e1ceb7f510 *libcxx-objects-v34.3.2-linux-x64.zip
|
||||
9bf3c6e8ad68f523fe086fada4148dd04e5bb0b9290d104873e66f2584a5cf50 *libcxx_headers.zip
|
||||
34e4b44f9c5e08b557a2caed55456ce7690abab910196a783a2a47b58d2b9ac9 *libcxxabi_headers.zip
|
||||
11f67635e6659f9188198e4086c51b89890b61a22f6c17c99eff35595ee8f51d *mksnapshot-v34.3.2-darwin-arm64.zip
|
||||
c0add9ef4ac27c73fa043d04b4c9635fd3fd9f5c16d7a03e961864ba05252813 *mksnapshot-v34.3.2-darwin-x64.zip
|
||||
6262adf86a340d8d28059937b01ef1117b93212e945fddbceea5c18d7e7c64f0 *mksnapshot-v34.3.2-linux-arm64-x64.zip
|
||||
f7db8ebe91a1cc8d24ef6aad12949a18d8e4975ac296e3e5e9ecd88c9bccb143 *mksnapshot-v34.3.2-linux-armv7l-x64.zip
|
||||
6642038e86bda362980ff1c8973a404e2b02efdd87de9e35b650fc1e743833da *mksnapshot-v34.3.2-linux-x64.zip
|
||||
15883bf8e8cd737c3682d1e719d7cbac92f50b525681aac324dca876861dfc7d *mksnapshot-v34.3.2-mas-arm64.zip
|
||||
4da23a950bfcc377ef21c37d496017ab4c36da03f3b41049ac114042c42608ce *mksnapshot-v34.3.2-mas-x64.zip
|
||||
fab59573d3c2f9bdf31146a1896d24ac0c51f736aad86d2f3c7ecef13c05a7fd *mksnapshot-v34.3.2-win32-arm64-x64.zip
|
||||
66f25e07c6f8d5d2009577a129440255a3baf63c929a5b60b2e77cd52e46105b *mksnapshot-v34.3.2-win32-ia32.zip
|
||||
8168bfbf61882cfac80aed1e71e364e1c7f2fccd11eac298e6abade8b46894ea *mksnapshot-v34.3.2-win32-x64.zip
|
||||
@@ -54,9 +54,11 @@ async function main(buildDir) {
|
||||
...defaultOpts,
|
||||
// TODO(deepak1556): Incorrectly declared type in electron-osx-sign
|
||||
ignore: (filePath) => {
|
||||
const ext = path_1.default.extname(filePath);
|
||||
return filePath.includes(gpuHelperAppName) ||
|
||||
filePath.includes(rendererHelperAppName) ||
|
||||
filePath.includes(pluginHelperAppName);
|
||||
filePath.includes(pluginHelperAppName) ||
|
||||
ext === '.asar' || ext === '.dat' || ext === '.gif' || ext === '.icns' || ext === '.ico' || ext === '.json' || ext === '.mp3' || ext === '.nib' || ext === '.pak' || ext === '.png' || ext === '.scpt' || ext === '.ttf' || ext === '.wasm' || ext === '.woff' || ext === '.woff2';
|
||||
}
|
||||
};
|
||||
const gpuHelperOpts = {
|
||||
|
||||
@@ -58,9 +58,11 @@ async function main(buildDir?: string): Promise<void> {
|
||||
...defaultOpts,
|
||||
// TODO(deepak1556): Incorrectly declared type in electron-osx-sign
|
||||
ignore: (filePath: string) => {
|
||||
const ext = path.extname(filePath);
|
||||
return filePath.includes(gpuHelperAppName) ||
|
||||
filePath.includes(rendererHelperAppName) ||
|
||||
filePath.includes(pluginHelperAppName);
|
||||
filePath.includes(pluginHelperAppName) ||
|
||||
ext === '.asar' || ext === '.dat' || ext === '.gif' || ext === '.icns' || ext === '.ico' || ext === '.json' || ext === '.mp3' || ext === '.nib' || ext === '.pak' || ext === '.png' || ext === '.scpt' || ext === '.ttf' || ext === '.wasm' || ext === '.woff' || ext === '.woff2';
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -139,6 +139,7 @@ module.exports.indentationFilter = [
|
||||
'!extensions/simple-browser/media/*.js',
|
||||
|
||||
'!extensions/open-remote-ssh/out/*.js', // Void added this
|
||||
'!extensions/open-remote-wsl/out/*.js', // Void added this
|
||||
];
|
||||
|
||||
module.exports.copyrightFilter = [
|
||||
|
||||
@@ -73,6 +73,7 @@ const compilations = [
|
||||
'.vscode/extensions/vscode-selfhost-import-aid/tsconfig.json',
|
||||
|
||||
'extensions/open-remote-ssh/tsconfig.json', // Void added this
|
||||
'extensions/open-remote-wsl/tsconfig.json', // Void added this
|
||||
|
||||
];
|
||||
|
||||
|
||||
+16
-4
@@ -154,9 +154,20 @@ function getNodeChecksum(expectedName) {
|
||||
}
|
||||
|
||||
function extractAlpinefromDocker(nodeVersion, platform, arch) {
|
||||
const imageName = arch === 'arm64' ? 'arm64v8/node' : 'node';
|
||||
let imageName = 'node';
|
||||
let dockerPlatform = '';
|
||||
|
||||
if (arch === 'arm64') {
|
||||
imageName = 'arm64v8/node';
|
||||
|
||||
const architecture = cp.execSync(`docker info --format '{{json .Architecture}}'`, { encoding: 'utf8' }).trim();
|
||||
if (architecture !== '"aarch64"') {
|
||||
dockerPlatform = '--platform=linux/arm64';
|
||||
}
|
||||
}
|
||||
|
||||
log(`Downloading node.js ${nodeVersion} ${platform} ${arch} from docker image ${imageName}`);
|
||||
const contents = cp.execSync(`docker run --rm ${imageName}:${nodeVersion}-alpine /bin/sh -c 'cat \`which node\`'`, { maxBuffer: 100 * 1024 * 1024, encoding: 'buffer' });
|
||||
const contents = cp.execSync(`docker run --rm ${dockerPlatform} ${imageName}:${nodeVersion}-alpine /bin/sh -c 'cat \`which node\`'`, { maxBuffer: 100 * 1024 * 1024, encoding: 'buffer' });
|
||||
return es.readArray([new File({ path: 'node', contents, stat: { mode: parseInt('755', 8) } })]);
|
||||
}
|
||||
|
||||
@@ -308,10 +319,11 @@ function packageTask(type, platform, arch, sourceFolderName, destinationFolderNa
|
||||
}
|
||||
|
||||
const name = product.nameShort;
|
||||
const release = packageJson.release;
|
||||
|
||||
let packageJsonContents;
|
||||
const packageJsonStream = gulp.src(['remote/package.json'], { base: 'remote' })
|
||||
.pipe(json({ name, version, dependencies: undefined, optionalDependencies: undefined, type: 'module' }))
|
||||
.pipe(json({ name, version, release, dependencies: undefined, optionalDependencies: undefined, type: 'module' }))
|
||||
.pipe(es.through(function (file) {
|
||||
packageJsonContents = file.contents.toString();
|
||||
this.emit('data', file);
|
||||
@@ -319,7 +331,7 @@ function packageTask(type, platform, arch, sourceFolderName, destinationFolderNa
|
||||
|
||||
let productJsonContents;
|
||||
const productJsonStream = gulp.src(['product.json'], { base: '.' })
|
||||
.pipe(json({ commit, date: readISODate('out-build'), version }))
|
||||
.pipe(json({ commit, date: readISODate('out-build'), version, release }))
|
||||
.pipe(es.through(function (file) {
|
||||
productJsonContents = file.contents.toString();
|
||||
this.emit('data', file);
|
||||
|
||||
@@ -267,7 +267,8 @@ function packageTask(platform, arch, sourceFolderName, destinationFolderName, op
|
||||
}
|
||||
|
||||
const name = product.nameShort;
|
||||
const packageJsonUpdates = { name, version };
|
||||
const release = packageJson.release;
|
||||
const packageJsonUpdates = { name, version, release };
|
||||
|
||||
if (platform === 'linux') {
|
||||
packageJsonUpdates.desktopName = `${product.applicationName}.desktop`;
|
||||
@@ -284,7 +285,7 @@ function packageTask(platform, arch, sourceFolderName, destinationFolderName, op
|
||||
// Void - this is important, creates the product.json in .app
|
||||
let productJsonContents;
|
||||
const productJsonStream = gulp.src(['product.json'], { base: '.' })
|
||||
.pipe(json({ commit, date: readISODate('out-build'), checksums, version }))
|
||||
.pipe(json({ commit, date: readISODate('out-build'), checksums, version, release }))
|
||||
.pipe(es.through(function (file) {
|
||||
productJsonContents = file.contents.toString();
|
||||
this.emit('data', file);
|
||||
@@ -372,7 +373,7 @@ function packageTask(platform, arch, sourceFolderName, destinationFolderName, op
|
||||
} else if (platform === 'darwin') {
|
||||
const shortcut = gulp.src('resources/darwin/bin/code.sh')
|
||||
.pipe(replace('@@APPNAME@@', product.applicationName))
|
||||
.pipe(rename('bin/code'));
|
||||
.pipe(rename('bin/' + product.applicationName));
|
||||
|
||||
all = es.merge(all, shortcut);
|
||||
}
|
||||
|
||||
@@ -25,7 +25,6 @@ const exec = util.promisify(cp.exec);
|
||||
const root = path.dirname(__dirname);
|
||||
const commit = getVersion(root);
|
||||
|
||||
const linuxPackageRevision = Math.floor(new Date().getTime() / 1000);
|
||||
|
||||
/**
|
||||
* @param {string} arch
|
||||
@@ -87,7 +86,7 @@ function prepareDebPackage(arch) {
|
||||
const dependencies = await dependenciesGenerator.getDependencies('deb', binaryDir, product.applicationName, debArch);
|
||||
gulp.src('resources/linux/debian/control.template', { base: '.' })
|
||||
.pipe(replace('@@NAME@@', product.applicationName))
|
||||
.pipe(replace('@@VERSION@@', packageJson.version + '-' + linuxPackageRevision))
|
||||
.pipe(replace('@@VERSION@@', `${packageJson.version}.${packageJson.release}`))
|
||||
.pipe(replace('@@ARCHITECTURE@@', debArch))
|
||||
.pipe(replace('@@DEPENDS@@', dependencies.join(', ')))
|
||||
.pipe(replace('@@RECOMMENDS@@', debianRecommendedDependencies.join(', ')))
|
||||
@@ -202,8 +201,7 @@ function prepareRpmPackage(arch) {
|
||||
.pipe(replace('@@NAME@@', product.applicationName))
|
||||
.pipe(replace('@@NAME_LONG@@', product.nameLong))
|
||||
.pipe(replace('@@ICON@@', product.linuxIconName))
|
||||
.pipe(replace('@@VERSION@@', packageJson.version))
|
||||
.pipe(replace('@@RELEASE@@', linuxPackageRevision))
|
||||
.pipe(replace('@@VERSION@@', `${packageJson.version}.${packageJson.release}`))
|
||||
.pipe(replace('@@ARCHITECTURE@@', rpmArch))
|
||||
.pipe(replace('@@LICENSE@@', product.licenseName))
|
||||
.pipe(replace('@@QUALITY@@', product.quality || '@@QUALITY@@'))
|
||||
@@ -279,7 +277,7 @@ function prepareSnapPackage(arch) {
|
||||
|
||||
const snapcraft = gulp.src('resources/linux/snap/snapcraft.yaml', { base: '.' })
|
||||
.pipe(replace('@@NAME@@', product.applicationName))
|
||||
.pipe(replace('@@VERSION@@', commit.substr(0, 8)))
|
||||
.pipe(replace('@@VERSION@@', `${packageJson.version}.${packageJson.release}`))
|
||||
// Possible run-on values https://snapcraft.io/docs/architectures
|
||||
.pipe(replace('@@ARCHITECTURE@@', arch === 'x64' ? 'amd64' : arch))
|
||||
.pipe(rename('snap/snapcraft.yaml'));
|
||||
|
||||
@@ -87,8 +87,8 @@ function buildWin32Setup(arch, target) {
|
||||
NameLong: product.nameLong,
|
||||
NameShort: product.nameShort,
|
||||
DirName: product.win32DirName,
|
||||
Version: pkg.version,
|
||||
RawVersion: pkg.version.replace(/-\w+$/, ''),
|
||||
Version: `${pkg.version}.${pkg.release}`,
|
||||
RawVersion: `${pkg.version.replace(/-\w+$/, '')}.${pkg.release}`,
|
||||
NameVersion: product.win32NameVersion + (target === 'user' ? ' (User)' : ''),
|
||||
ExeBasename: product.nameShort,
|
||||
RegValueName: product.win32RegValueName,
|
||||
|
||||
@@ -85,10 +85,7 @@ function getExtensionDownloadStream(extension) {
|
||||
if (extension.vsix) {
|
||||
input = ext.fromVsix(path_1.default.join(root, extension.vsix), extension);
|
||||
}
|
||||
else if (productjson.extensionsGallery?.serviceUrl) {
|
||||
input = ext.fromMarketplace(productjson.extensionsGallery.serviceUrl, extension);
|
||||
}
|
||||
else {
|
||||
else { // Void - ext-from-gh.patch
|
||||
input = ext.fromGithub(extension);
|
||||
}
|
||||
return input.pipe((0, gulp_rename_1.default)(p => p.dirname = `${extension.name}/${p.dirname}`));
|
||||
|
||||
@@ -73,9 +73,7 @@ function getExtensionDownloadStream(extension: IExtensionDefinition) {
|
||||
|
||||
if (extension.vsix) {
|
||||
input = ext.fromVsix(path.join(root, extension.vsix), extension);
|
||||
} else if (productjson.extensionsGallery?.serviceUrl) {
|
||||
input = ext.fromMarketplace(productjson.extensionsGallery.serviceUrl, extension);
|
||||
} else {
|
||||
} else { // Void - ext-from-gh.patch
|
||||
input = ext.fromGithub(extension);
|
||||
}
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ const dirs = [
|
||||
'.vscode/extensions/vscode-selfhost-import-aid',
|
||||
'.vscode/extensions/vscode-selfhost-test-provider',
|
||||
'extensions/open-remote-ssh', // Void added this
|
||||
'extensions/open-remote-wsl', // Void added this
|
||||
|
||||
];
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ function getMangledFileContents(projectPath) {
|
||||
* @type {webpack.LoaderDefinitionFunction}
|
||||
*/
|
||||
module.exports = async function (source, sourceMap, meta) {
|
||||
if (this.mode !== 'production') {
|
||||
if (true) { // Void - extensions-disable-mangler
|
||||
// Only enable mangling in production builds
|
||||
return source;
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
"type": "string",
|
||||
"description": "The URL from where the vscode server will be downloaded. You can use the following variables and they will be replaced dynamically:\n- ${quality}: vscode server quality, e.g. stable or insiders\n- ${version}: vscode server version, e.g. 1.69.0\n- ${commit}: vscode server release commit\n- ${arch}: vscode server arch, e.g. x64, armhf, arm64\n- ${release}: release number",
|
||||
"scope": "application",
|
||||
"default": "https://github.com/voideditor/${NAME_OF_REPO}/releases/download/${version}.${release}/void-server-${os}-${arch}-${version}.${release}.tar.gz"
|
||||
"default": "https://github.com/voideditor/binaries/releases/download/${version}.${release}/void-reh-${os}-${arch}-${version}.${release}.tar.gz"
|
||||
},
|
||||
"remote.SSH.remotePlatform": {
|
||||
"type": "object",
|
||||
|
||||
@@ -32,12 +32,14 @@ export async function getVSCodeServerConfig(): Promise<IServerConfig> {
|
||||
const customServerBinaryName = vscode.workspace.getConfiguration('remote.SSH.experimental').get<string>('serverBinaryName', '');
|
||||
|
||||
return {
|
||||
version: vscode.version.replace('-insider', ''),
|
||||
// version: vscode.version.replace('-insider', ''),
|
||||
commit: productJson.commit,
|
||||
quality: productJson.quality,
|
||||
release: productJson.release,
|
||||
serverApplicationName: customServerBinaryName || productJson.serverApplicationName,
|
||||
serverDataFolderName: productJson.serverDataFolderName,
|
||||
serverDownloadUrlTemplate: productJson.serverDownloadUrlTemplate
|
||||
serverDownloadUrlTemplate: productJson.serverDownloadUrlTemplate,
|
||||
// Void changed this
|
||||
version: productJson.voidVersion
|
||||
};
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ export class ServerInstallError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_DOWNLOAD_URL_TEMPLATE = 'https://github.com/voideditor/void-updates-server/releases/download/test/void-server-${os}-${arch}.tar.gz';
|
||||
const DEFAULT_DOWNLOAD_URL_TEMPLATE = 'https://github.com/voideditor/binaries/releases/download/${version}.${release}/void-reh-${os}-${arch}-${version}.${release}.tar.gz';
|
||||
|
||||
export async function installCodeServer(conn: SSHConnection, serverDownloadUrlTemplate: string | undefined, extensionIds: string[], envVariables: string[], platform: string | undefined, useSocketPath: boolean, logger: Log): Promise<ServerInstallResult> {
|
||||
let shell = 'powershell';
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# Remote - WSL Support
|
||||
|
||||
Inherited for Void from [Open Remote - WSL](https://github.com/jeanp413/open-remote-wsl).
|
||||
@@ -0,0 +1,17 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
//@ts-check
|
||||
|
||||
'use strict';
|
||||
|
||||
const withBrowserDefaults = require('../shared.webpack.config').browser;
|
||||
|
||||
module.exports = withBrowserDefaults({
|
||||
context: __dirname,
|
||||
entry: {
|
||||
extension: './src/extension.ts'
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
//@ts-check
|
||||
|
||||
'use strict';
|
||||
|
||||
const withDefaults = require('../shared.webpack.config');
|
||||
|
||||
module.exports = withDefaults({
|
||||
context: __dirname,
|
||||
resolve: {
|
||||
mainFields: ['module', 'main']
|
||||
},
|
||||
entry: {
|
||||
extension: './src/extension.ts',
|
||||
}
|
||||
});
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "open-remote-wsl",
|
||||
"version": "0.0.4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "open-remote-wsl",
|
||||
"version": "0.0.4",
|
||||
"engines": {
|
||||
"vscode": "^1.70.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
{
|
||||
"name": "open-remote-wsl",
|
||||
"displayName": "Remote - WSL",
|
||||
"description": "Open any folder in the Windows Subsystem for Linux (WSL).",
|
||||
"version": "0.0.4",
|
||||
"icon": "resources/icon.png",
|
||||
"engines": {
|
||||
"vscode": "^1.70.2"
|
||||
},
|
||||
"extensionKind": [
|
||||
"ui"
|
||||
],
|
||||
"enabledApiProposals": [
|
||||
"resolvers",
|
||||
"contribViewsRemote"
|
||||
],
|
||||
"keywords": [
|
||||
"remote development",
|
||||
"remote",
|
||||
"wsl"
|
||||
],
|
||||
"api": "none",
|
||||
"activationEvents": [
|
||||
"onCommand:openremotewsl.connect",
|
||||
"onCommand:openremotewsl.connectInNewWindow",
|
||||
"onCommand:openremotewsl.connectUsingDistro",
|
||||
"onCommand:openremotewsl.connectUsingDistroInNewWindow",
|
||||
"onCommand:openremotewsl.showLog",
|
||||
"onResolveRemoteAuthority:wsl",
|
||||
"onView:wslTargets"
|
||||
],
|
||||
"main": "./out/extension.js",
|
||||
"contributes": {
|
||||
"configuration": {
|
||||
"title": "WSL",
|
||||
"properties": {
|
||||
"remote.WSL.serverDownloadUrlTemplate": {
|
||||
"type": "string",
|
||||
"description": "The URL from where the vscode server will be downloaded. You can use the following variables and they will be replaced dynamically:\n- ${quality}: vscode server quality, e.g. stable or insiders\n- ${version}: vscode server version, e.g. 1.69.0\n- ${commit}: vscode server release commit\n- ${arch}: vscode server arch, e.g. x64, armhf, arm64\n- ${release}: release number",
|
||||
"scope": "application",
|
||||
"default": "https://github.com/voideditor/binaries/releases/download/${version}.${release}/void-reh-${os}-${arch}-${version}.${release}.tar.gz"
|
||||
}
|
||||
}
|
||||
},
|
||||
"views": {
|
||||
"remote": [
|
||||
{
|
||||
"id": "wslTargets",
|
||||
"name": "WSL Targets",
|
||||
"group": "targets@1",
|
||||
"when": "(isWindows && !isWeb)",
|
||||
"remoteName": "wsl"
|
||||
}
|
||||
]
|
||||
},
|
||||
"commands": [
|
||||
{
|
||||
"command": "openremotewsl.connect",
|
||||
"title": "Connect to WSL",
|
||||
"category": "Remote-WSL"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.connectInNewWindow",
|
||||
"title": "Connect to WSL in New Window",
|
||||
"category": "Remote-WSL"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.connectUsingDistro",
|
||||
"title": "Connect to WSL using Distro...",
|
||||
"category": "Remote-WSL"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.connectUsingDistroInNewWindow",
|
||||
"title": "Connect to WSL using Distro in New Window...",
|
||||
"category": "Remote-WSL"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.showLog",
|
||||
"title": "Show Log",
|
||||
"category": "Remote-WSL"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.emptyWindowInNewWindow",
|
||||
"title": "Connect in New Window",
|
||||
"icon": "$(empty-window)"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.emptyWindowInCurrentWindow",
|
||||
"title": "Connect in Current Window"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.reopenFolderInCurrentWindow",
|
||||
"title": "Open in Current Window"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.reopenFolderInNewWindow",
|
||||
"title": "Open in New Window",
|
||||
"icon": "$(folder-opened)"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.deleteFolderHistoryItem",
|
||||
"title": "Remove From Recent List",
|
||||
"icon": "$(x)"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.refresh",
|
||||
"title": "Refresh",
|
||||
"icon": "$(refresh)"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.addDistro",
|
||||
"title": "Add a Distro",
|
||||
"icon": "$(plus)"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.setDefaultDistro",
|
||||
"title": "Set as Default Distro"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.deleteDistro",
|
||||
"title": "Delete Distro"
|
||||
}
|
||||
],
|
||||
"resourceLabelFormatters": [
|
||||
{
|
||||
"scheme": "vscode-remote",
|
||||
"authority": "wsl+*",
|
||||
"formatting": {
|
||||
"label": "${path}",
|
||||
"separator": "/",
|
||||
"tildify": true,
|
||||
"workspaceSuffix": "WSL"
|
||||
}
|
||||
}
|
||||
],
|
||||
"menus": {
|
||||
"statusBar/remoteIndicator": [
|
||||
{
|
||||
"command": "openremotewsl.connect",
|
||||
"when": "(isWindows && !isWeb)",
|
||||
"group": "remote_20_wsl_1general@1"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.connectUsingDistro",
|
||||
"when": "(isWindows && !isWeb)",
|
||||
"group": "remote_20_wsl_1general@2"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.showLog",
|
||||
"when": "remoteName =~ /^wsl$/",
|
||||
"group": "remote_20_wsl_1general@4"
|
||||
}
|
||||
],
|
||||
"commandPalette": [
|
||||
{
|
||||
"command": "openremotewsl.connect",
|
||||
"when": "(isWindows && !isWeb)"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.connectInNewWindow",
|
||||
"when": "(isWindows && !isWeb)"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.connectUsingDistro",
|
||||
"when": "(isWindows && !isWeb)"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.connectUsingDistroInNewWindow",
|
||||
"when": "(isWindows && !isWeb)"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.refresh",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.addDistro",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.emptyWindowInNewWindow",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.emptyWindowInCurrentWindow",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.reopenFolderInCurrentWindow",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.reopenFolderInNewWindow",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.deleteFolderHistoryItem",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.setDefaultDistro",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.deleteDistro",
|
||||
"when": "false"
|
||||
}
|
||||
],
|
||||
"view/title": [
|
||||
{
|
||||
"command": "openremotewsl.explorer.addDistro",
|
||||
"when": "view == wslTargets",
|
||||
"group": "navigation"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.refresh",
|
||||
"when": "view == wslTargets",
|
||||
"group": "navigation"
|
||||
}
|
||||
],
|
||||
"view/item/context": [
|
||||
{
|
||||
"command": "openremotewsl.explorer.emptyWindowInNewWindow",
|
||||
"when": "viewItem =~ /^openremotewsl.explorer.distro$/",
|
||||
"group": "inline@1"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.emptyWindowInNewWindow",
|
||||
"when": "viewItem =~ /^openremotewsl.explorer.distro$/",
|
||||
"group": "navigation@2"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.emptyWindowInCurrentWindow",
|
||||
"when": "viewItem =~ /^openremotewsl.explorer.distro$/",
|
||||
"group": "navigation@1"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.setDefaultDistro",
|
||||
"when": "viewItem =~ /^openremotewsl.explorer.distro$/",
|
||||
"group": "management@1"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.deleteDistro",
|
||||
"when": "viewItem =~ /^openremotewsl.explorer.distro$/",
|
||||
"group": "management@2"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.reopenFolderInNewWindow",
|
||||
"when": "viewItem == openremotewsl.explorer.folder",
|
||||
"group": "inline@1"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.reopenFolderInNewWindow",
|
||||
"when": "viewItem == openremotewsl.explorer.folder",
|
||||
"group": "navigation@2"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.reopenFolderInCurrentWindow",
|
||||
"when": "viewItem == openremotewsl.explorer.folder",
|
||||
"group": "navigation@1"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.deleteFolderHistoryItem",
|
||||
"when": "viewItem =~ /^openremotewsl.explorer.folder/",
|
||||
"group": "navigation@3"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.deleteFolderHistoryItem",
|
||||
"when": "viewItem =~ /^openremotewsl.explorer.folder/",
|
||||
"group": "inline@2"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"vscode:prepublish": "npm run compile",
|
||||
"compile": "gulp compile-extension:open-remote-wsl",
|
||||
"compile-web": "npx webpack-cli --config extension-browser.webpack.config --mode none",
|
||||
"watch": "gulp watch-extension:open-remote-wsl",
|
||||
"watch-web": "npx webpack-cli --config extension-browser.webpack.config --mode none --watch --info-verbosity verbose"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import Log from './common/logger';
|
||||
import { installCodeServer, ServerInstallError } from './serverSetup';
|
||||
import { WSLManager } from './wsl/wslManager';
|
||||
|
||||
export const REMOTE_WSL_AUTHORITY = 'wsl';
|
||||
|
||||
export function getRemoteAuthority(distro: string) {
|
||||
return `${REMOTE_WSL_AUTHORITY}+${distro}`;
|
||||
}
|
||||
|
||||
class Tunnel implements vscode.Tunnel {
|
||||
private _onDidDisposeEmitter = new vscode.EventEmitter<void>();
|
||||
|
||||
readonly onDidDispose = this._onDidDisposeEmitter.event;
|
||||
|
||||
constructor(
|
||||
readonly remoteAddress: { port: number; host: string },
|
||||
readonly localAddress: { port: number; host: string }
|
||||
) {
|
||||
// If ipv6 localhost 0:0:0:0:0:0:0:1 or [::1] replace with localhost
|
||||
if (localAddress.host !== 'localhost' && localAddress.host !== '127.0.0.1') {
|
||||
localAddress.host = 'localhost';
|
||||
}
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._onDidDisposeEmitter.fire();
|
||||
}
|
||||
}
|
||||
|
||||
export class RemoteWSLResolver implements vscode.RemoteAuthorityResolver, vscode.Disposable {
|
||||
|
||||
private labelFormatterDisposable: vscode.Disposable | undefined;
|
||||
|
||||
constructor(
|
||||
private readonly wslManager: WSLManager,
|
||||
private readonly logger: Log
|
||||
) {
|
||||
}
|
||||
|
||||
resolve(authority: string, context: vscode.RemoteAuthorityResolverContext): Thenable<vscode.ResolverResult> {
|
||||
const [type, distroName] = authority.split('+');
|
||||
if (type !== REMOTE_WSL_AUTHORITY) {
|
||||
throw new Error(`Invalid authority type for WSL resolver: ${type}`);
|
||||
}
|
||||
|
||||
this.logger.info(`Resolving wsl remote authority '${authority}' (attemp #${context.resolveAttempt})`);
|
||||
|
||||
// It looks like default values are not loaded yet when resolving a remote,
|
||||
// so let's hardcode the default values here
|
||||
const remoteSSHconfig = vscode.workspace.getConfiguration('remote.WSL');
|
||||
const serverDownloadUrlTemplate = remoteSSHconfig.get<string>('serverDownloadUrlTemplate');
|
||||
|
||||
return vscode.window.withProgress({
|
||||
title: `Setting up WSL Distro: ${distroName}`,
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
cancellable: false
|
||||
}, async () => {
|
||||
try {
|
||||
const installResult = await installCodeServer(this.wslManager, distroName, serverDownloadUrlTemplate, [], [], this.logger);
|
||||
|
||||
this.labelFormatterDisposable?.dispose();
|
||||
this.labelFormatterDisposable = vscode.workspace.registerResourceLabelFormatter({
|
||||
scheme: 'vscode-remote',
|
||||
authority: `${REMOTE_WSL_AUTHORITY}+*`,
|
||||
formatting: {
|
||||
label: '${path}',
|
||||
separator: '/',
|
||||
tildify: true,
|
||||
workspaceSuffix: `WSL: ${distroName}`,
|
||||
workspaceTooltip: `Running in ${distroName}`
|
||||
}
|
||||
});
|
||||
|
||||
return new vscode.ResolvedAuthority('127.0.0.1', installResult.listeningOn, installResult.connectionToken);
|
||||
} catch (e: unknown) {
|
||||
this.logger.error(`Error resolving authority`, e);
|
||||
|
||||
// Initial connection
|
||||
if (context.resolveAttempt === 1) {
|
||||
this.logger.show();
|
||||
|
||||
const closeRemote = 'Close Remote';
|
||||
const retry = 'Retry';
|
||||
const result = await vscode.window.showErrorMessage(`Could not establish connection to WSL distro "${distroName}"`, { modal: true }, closeRemote, retry);
|
||||
if (result === closeRemote) {
|
||||
await vscode.commands.executeCommand('workbench.action.remote.close');
|
||||
} else if (result === retry) {
|
||||
await vscode.commands.executeCommand('workbench.action.reloadWindow');
|
||||
}
|
||||
}
|
||||
|
||||
if (e instanceof ServerInstallError || !(e instanceof Error)) {
|
||||
throw vscode.RemoteAuthorityResolverError.NotAvailable(e instanceof Error ? e.message : String(e));
|
||||
} else {
|
||||
throw vscode.RemoteAuthorityResolverError.TemporarilyNotAvailable(e.message);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async tunnelFactory(tunnelOptions: vscode.TunnelOptions) {
|
||||
return new Tunnel(
|
||||
tunnelOptions.remoteAddress,
|
||||
{
|
||||
host: tunnelOptions.remoteAddress.host,
|
||||
port: tunnelOptions.localAddressPort ?? tunnelOptions.remoteAddress.port
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.labelFormatterDisposable?.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { getRemoteAuthority } from './authResolver';
|
||||
import { WSLDistro, WSLManager, WSLOnlineDistro } from './wsl/wslManager';
|
||||
import wslTerminal from './wsl/wslTerminal';
|
||||
|
||||
async function showDistrosPicker(wslManager: WSLManager, placeHolder: string): Promise<WSLDistro | undefined> {
|
||||
const pickItemsPromise = wslManager.listDistros()
|
||||
.then(distros => distros.map(distroData => {
|
||||
return {
|
||||
...distroData,
|
||||
label: `${distroData.name}`,
|
||||
detail: distroData.isDefault ? 'default distro' : undefined,
|
||||
};
|
||||
}));
|
||||
|
||||
const picked = await vscode.window.showQuickPick(pickItemsPromise, { canPickMany: false, placeHolder });
|
||||
return picked;
|
||||
}
|
||||
|
||||
async function showOnlineDistrosPicker(wslManager: WSLManager, placeHolder: string): Promise<WSLOnlineDistro | undefined> {
|
||||
const pickItemsPromise = Promise.all([wslManager.listOnlineDistros(), wslManager.listDistros()])
|
||||
.then(([onlineDistros, localDistros]) => {
|
||||
const distroToInstall = onlineDistros.filter(d => !localDistros.some(l => l.name === d.name));
|
||||
return distroToInstall.map(distroData => {
|
||||
return {
|
||||
...distroData,
|
||||
label: `${distroData.friendlyName}`,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
const picked = await vscode.window.showQuickPick(pickItemsPromise, { canPickMany: false, placeHolder });
|
||||
return picked;
|
||||
}
|
||||
|
||||
export async function promptOpenRemoteWSLWindow(wslManager: WSLManager, useDefault: boolean, reuseWindow: boolean) {
|
||||
let distroName: string | undefined;
|
||||
if (useDefault) {
|
||||
const distros = await wslManager.listDistros();
|
||||
distroName = distros.find(distro => distro.isDefault)?.name;
|
||||
} else {
|
||||
distroName = (await showDistrosPicker(wslManager, 'Select WSL distro'))?.name;
|
||||
}
|
||||
|
||||
if (!distroName) {
|
||||
return;
|
||||
}
|
||||
|
||||
openRemoteWSLWindow(distroName, reuseWindow);
|
||||
}
|
||||
|
||||
export async function promptInstallNewWSLDistro(wslManager: WSLManager) {
|
||||
const distroName = (await showOnlineDistrosPicker(wslManager, 'Select the WSL distro to install'))?.name;
|
||||
if (!distroName) {
|
||||
return;
|
||||
}
|
||||
|
||||
wslTerminal.runCommand(`wsl.exe --install -d ${distroName}`);
|
||||
}
|
||||
|
||||
export function openRemoteWSLWindow(distro: string, reuseWindow: boolean) {
|
||||
vscode.commands.executeCommand('vscode.newWindow', { remoteAuthority: getRemoteAuthority(distro), reuseWindow });
|
||||
}
|
||||
|
||||
export function openRemoteWSLLocationWindow(distro: string, path: string, reuseWindow: boolean) {
|
||||
vscode.commands.executeCommand('vscode.openFolder', vscode.Uri.from({ scheme: 'vscode-remote', authority: getRemoteAuthority(distro), path }), { forceNewWindow: !reuseWindow });
|
||||
}
|
||||
|
||||
export async function setDefaultWSLDistro(wslManager: WSLManager, distroName: string) {
|
||||
await wslManager.setDefaultDistro(distroName);
|
||||
}
|
||||
|
||||
export async function deleteWSLDistro(wslManager: WSLManager, distroName: string) {
|
||||
const deleteAction = 'Delete';
|
||||
const resp = await vscode.window.showInformationMessage(`Are you sure you want to permanently delete the distro "${distroName}" including all its data?`, { modal: true }, deleteAction);
|
||||
if (resp === deleteAction) {
|
||||
await wslManager.deleteDistro(distroName);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export function timeout(millis: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, millis));
|
||||
}
|
||||
|
||||
export interface ITask<T> {
|
||||
(): T;
|
||||
}
|
||||
|
||||
export async function retry<T>(task: ITask<Promise<T>>, delay: number, retries: number): Promise<T> {
|
||||
let lastError: Error | undefined;
|
||||
|
||||
for (let i = 0; i < retries; i++) {
|
||||
try {
|
||||
return await task();
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
|
||||
await timeout(delay);
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
export function disposeAll(disposables: vscode.Disposable[]): void {
|
||||
while (disposables.length) {
|
||||
const item = disposables.pop();
|
||||
if (item) {
|
||||
item.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class Disposable {
|
||||
private _isDisposed = false;
|
||||
|
||||
protected _disposables: vscode.Disposable[] = [];
|
||||
|
||||
public dispose(): any {
|
||||
if (this._isDisposed) {
|
||||
return;
|
||||
}
|
||||
this._isDisposed = true;
|
||||
disposeAll(this._disposables);
|
||||
}
|
||||
|
||||
protected _register<T extends vscode.Disposable>(value: T): T {
|
||||
if (this._isDisposed) {
|
||||
value.dispose();
|
||||
} else {
|
||||
this._disposables.push(value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
protected get isDisposed(): boolean {
|
||||
return this._isDisposed;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export interface IDisposable {
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
export interface Event<T> {
|
||||
(listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[]): IDisposable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a promise that resolves when the event fires, or when cancellation
|
||||
* is requested, whichever happens first.
|
||||
*/
|
||||
export function toPromise<T>(event: Event<T>): Promise<T>;
|
||||
export function toPromise<T>(event: Event<T>, signal: AbortSignal): Promise<T | undefined>;
|
||||
export function toPromise<T>(event: Event<T>, signal?: AbortSignal): Promise<T | undefined> {
|
||||
if (!signal) {
|
||||
return new Promise<T>((resolve) => once(event, resolve));
|
||||
}
|
||||
|
||||
if (signal.aborted) {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const d2 = once(event, (data) => {
|
||||
(signal as any).removeEventListener('abort', d1);
|
||||
resolve(data);
|
||||
});
|
||||
|
||||
const d1 = () => {
|
||||
d2.dispose();
|
||||
(signal as any).removeEventListener('abort', d1);
|
||||
resolve(undefined);
|
||||
};
|
||||
|
||||
(signal as any).addEventListener('abort', d1);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a handler that handles one event on the emitter, then disposes itself.
|
||||
*/
|
||||
export const once = <T>(event: Event<T>, listener: (data: T) => void): IDisposable => {
|
||||
const disposable = event((value) => {
|
||||
listener(value);
|
||||
disposable.dispose();
|
||||
});
|
||||
|
||||
return disposable;
|
||||
};
|
||||
|
||||
/**
|
||||
* Base event emitter. Calls listeners when data is emitted.
|
||||
*/
|
||||
export class EventEmitter<T> {
|
||||
private listeners?: Array<(data: T) => void> | ((data: T) => void);
|
||||
|
||||
/**
|
||||
* Event<T> function.
|
||||
*/
|
||||
public readonly event: Event<T> = (listener, thisArgs, disposables) => {
|
||||
const d = this.add(thisArgs ? listener.bind(thisArgs) : listener);
|
||||
disposables?.push(d);
|
||||
return d;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the number of event listeners.
|
||||
*/
|
||||
public get size() {
|
||||
if (!this.listeners) {
|
||||
return 0;
|
||||
} else if (typeof this.listeners === 'function') {
|
||||
return 1;
|
||||
} else {
|
||||
return this.listeners.length;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits event data.
|
||||
*/
|
||||
public fire(value: T) {
|
||||
if (!this.listeners) {
|
||||
// no-op
|
||||
} else if (typeof this.listeners === 'function') {
|
||||
this.listeners(value);
|
||||
} else {
|
||||
for (const listener of this.listeners) {
|
||||
listener(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disposes of the emitter.
|
||||
*/
|
||||
public dispose() {
|
||||
this.listeners = undefined;
|
||||
}
|
||||
|
||||
private add(listener: (data: T) => void): IDisposable {
|
||||
if (!this.listeners) {
|
||||
this.listeners = listener;
|
||||
} else if (typeof this.listeners === 'function') {
|
||||
this.listeners = [this.listeners, listener];
|
||||
} else {
|
||||
this.listeners.push(listener);
|
||||
}
|
||||
|
||||
return { dispose: () => this.rm(listener) };
|
||||
}
|
||||
|
||||
private rm(listener: (data: T) => void) {
|
||||
if (!this.listeners) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof this.listeners === 'function') {
|
||||
if (this.listeners === listener) {
|
||||
this.listeners = undefined;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const index = this.listeners.indexOf(listener);
|
||||
if (index === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.listeners.length === 2) {
|
||||
this.listeners = index === 0 ? this.listeners[1] : this.listeners[0];
|
||||
} else {
|
||||
this.listeners = this.listeners.slice(0, index).concat(this.listeners.slice(index + 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
|
||||
const homeDir = os.homedir();
|
||||
|
||||
export async function exists(path: string) {
|
||||
try {
|
||||
await fs.promises.access(path);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function untildify(path: string) {
|
||||
return path.replace(/^~(?=$|\/|\\)/, homeDir);
|
||||
}
|
||||
|
||||
export function normalizeToSlash(path: string) {
|
||||
return path.replace(/\\/g, '/');
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
type LogLevel = 'Trace' | 'Info' | 'Error';
|
||||
|
||||
export default class Log {
|
||||
private output: vscode.OutputChannel;
|
||||
|
||||
constructor(name: string) {
|
||||
this.output = vscode.window.createOutputChannel(name);
|
||||
}
|
||||
|
||||
private data2String(data: any): string {
|
||||
if (data instanceof Error) {
|
||||
return data.stack || data.message;
|
||||
}
|
||||
if (data.success === false && data.message) {
|
||||
return data.message;
|
||||
}
|
||||
return data.toString();
|
||||
}
|
||||
|
||||
public trace(message: string, data?: any): void {
|
||||
this.logLevel('Trace', message, data);
|
||||
}
|
||||
|
||||
public info(message: string, data?: any): void {
|
||||
this.logLevel('Info', message, data);
|
||||
}
|
||||
|
||||
public error(message: string, data?: any): void {
|
||||
this.logLevel('Error', message, data);
|
||||
}
|
||||
|
||||
public logLevel(level: LogLevel, message: string, data?: any): void {
|
||||
this.output.appendLine(`[${level} - ${this.now()}] ${message}`);
|
||||
if (data) {
|
||||
this.output.appendLine(this.data2String(data));
|
||||
}
|
||||
}
|
||||
|
||||
private now(): string {
|
||||
const now = new Date();
|
||||
return padLeft(now.getUTCHours() + '', 2, '0')
|
||||
+ ':' + padLeft(now.getMinutes() + '', 2, '0')
|
||||
+ ':' + padLeft(now.getUTCSeconds() + '', 2, '0') + '.' + now.getMilliseconds();
|
||||
}
|
||||
|
||||
public show() {
|
||||
this.output.show();
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
this.output.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
function padLeft(s: string, n: number, pad = ' ') {
|
||||
return pad.repeat(Math.max(0, n - s.length)) + s;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export const isWindows = process.platform === 'win32';
|
||||
export const isMacintosh = process.platform === 'darwin';
|
||||
export const isLinux = process.platform === 'linux';
|
||||
@@ -0,0 +1,134 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as net from 'net';
|
||||
|
||||
/**
|
||||
* Finds a random unused port assigned by the operating system. Will reject in case no free port can be found.
|
||||
*/
|
||||
export function findRandomPort(): Promise<number> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const server = net.createServer({ pauseOnConnect: true });
|
||||
server.on('error', reject);
|
||||
server.on('listening', () => {
|
||||
const port = (server.address() as net.AddressInfo).port;
|
||||
server.close(() => resolve(port));
|
||||
});
|
||||
server.listen(0, '127.0.0.1');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a start point and a max number of retries, will find a port that
|
||||
* is openable. Will return 0 in case no free port can be found.
|
||||
*/
|
||||
export function findFreePort(startPort: number, giveUpAfter: number, timeout: number, stride = 1): Promise<number> {
|
||||
let done = false;
|
||||
|
||||
return new Promise(resolve => {
|
||||
const timeoutHandle = setTimeout(() => {
|
||||
if (!done) {
|
||||
done = true;
|
||||
return resolve(0);
|
||||
}
|
||||
}, timeout);
|
||||
|
||||
doFindFreePort(startPort, giveUpAfter, stride, (port) => {
|
||||
if (!done) {
|
||||
done = true;
|
||||
clearTimeout(timeoutHandle);
|
||||
return resolve(port);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function doFindFreePort(startPort: number, giveUpAfter: number, stride: number, clb: (port: number) => void): void {
|
||||
if (giveUpAfter === 0) {
|
||||
return clb(0);
|
||||
}
|
||||
|
||||
const client = new net.Socket();
|
||||
|
||||
// If we can connect to the port it means the port is already taken so we continue searching
|
||||
client.once('connect', () => {
|
||||
dispose(client);
|
||||
|
||||
return doFindFreePort(startPort + stride, giveUpAfter - 1, stride, clb);
|
||||
});
|
||||
|
||||
client.once('data', () => {
|
||||
// this listener is required since node.js 8.x
|
||||
});
|
||||
|
||||
client.once('error', (err: Error & { code?: string }) => {
|
||||
dispose(client);
|
||||
|
||||
// If we receive any non ECONNREFUSED error, it means the port is used but we cannot connect
|
||||
if (err.code !== 'ECONNREFUSED') {
|
||||
return doFindFreePort(startPort + stride, giveUpAfter - 1, stride, clb);
|
||||
}
|
||||
|
||||
// Otherwise it means the port is free to use!
|
||||
return clb(startPort);
|
||||
});
|
||||
|
||||
client.connect(startPort, '127.0.0.1');
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses listen instead of connect. Is faster, but if there is another listener on 0.0.0.0 then this will take 127.0.0.1 from that listener.
|
||||
*/
|
||||
export function findFreePortFaster(startPort: number, giveUpAfter: number, timeout: number): Promise<number> {
|
||||
let resolved = false;
|
||||
let timeoutHandle: NodeJS.Timeout | undefined = undefined;
|
||||
let countTried = 1;
|
||||
const server = net.createServer({ pauseOnConnect: true });
|
||||
function doResolve(port: number, resolve: (port: number) => void) {
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
server.removeAllListeners();
|
||||
server.close();
|
||||
if (timeoutHandle) {
|
||||
clearTimeout(timeoutHandle);
|
||||
}
|
||||
resolve(port);
|
||||
}
|
||||
}
|
||||
return new Promise<number>(resolve => {
|
||||
timeoutHandle = setTimeout(() => {
|
||||
doResolve(0, resolve);
|
||||
}, timeout);
|
||||
|
||||
server.on('listening', () => {
|
||||
doResolve(startPort, resolve);
|
||||
});
|
||||
server.on('error', err => {
|
||||
if (err && ((<any>err).code === 'EADDRINUSE' || (<any>err).code === 'EACCES') && (countTried < giveUpAfter)) {
|
||||
startPort++;
|
||||
countTried++;
|
||||
server.listen(startPort, '127.0.0.1');
|
||||
} else {
|
||||
doResolve(0, resolve);
|
||||
}
|
||||
});
|
||||
server.on('close', () => {
|
||||
doResolve(0, resolve);
|
||||
});
|
||||
server.listen(startPort, '127.0.0.1');
|
||||
});
|
||||
}
|
||||
|
||||
function dispose(socket: net.Socket): void {
|
||||
try {
|
||||
socket.removeAllListeners('connect');
|
||||
socket.removeAllListeners('error');
|
||||
socket.end();
|
||||
socket.destroy();
|
||||
socket.unref();
|
||||
} catch (error) {
|
||||
console.error(error); // otherwise this error would get lost in the callback chain
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
import { RemoteLocationHistory } from './remoteLocationHistory';
|
||||
import { Disposable } from './common/disposable';
|
||||
import { openRemoteWSLWindow, openRemoteWSLLocationWindow, promptInstallNewWSLDistro, deleteWSLDistro, setDefaultWSLDistro } from './commands';
|
||||
import { WSLManager } from './wsl/wslManager';
|
||||
|
||||
class DistroItem {
|
||||
constructor(
|
||||
public name: string,
|
||||
public isDefault: boolean,
|
||||
public locations: string[]
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
class DistroLocationItem {
|
||||
constructor(
|
||||
public path: string,
|
||||
public name: string
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
type DataTreeItem = DistroItem | DistroLocationItem;
|
||||
|
||||
export class DistroTreeDataProvider extends Disposable implements vscode.TreeDataProvider<DataTreeItem> {
|
||||
|
||||
private readonly _onDidChangeTreeData = this._register(new vscode.EventEmitter<DataTreeItem | DataTreeItem[] | void>());
|
||||
public readonly onDidChangeTreeData = this._onDidChangeTreeData.event;
|
||||
|
||||
constructor(
|
||||
private readonly locationHistory: RemoteLocationHistory,
|
||||
private readonly wslManager: WSLManager
|
||||
) {
|
||||
super();
|
||||
|
||||
this._register(vscode.commands.registerCommand('openremotewsl.explorer.addDistro', () => promptInstallNewWSLDistro(wslManager)));
|
||||
this._register(vscode.commands.registerCommand('openremotewsl.explorer.refresh', () => this.refresh()));
|
||||
this._register(vscode.commands.registerCommand('openremotewsl.explorer.emptyWindowInNewWindow', e => this.openRemoteWSLWindow(e, false)));
|
||||
this._register(vscode.commands.registerCommand('openremotewsl.explorer.emptyWindowInCurrentWindow', e => this.openRemoteWSLWindow(e, true)));
|
||||
this._register(vscode.commands.registerCommand('openremotewsl.explorer.reopenFolderInNewWindow', e => this.openRemoteWSLocationWindow(e, false)));
|
||||
this._register(vscode.commands.registerCommand('openremotewsl.explorer.reopenFolderInCurrentWindow', e => this.openRemoteWSLocationWindow(e, true)));
|
||||
this._register(vscode.commands.registerCommand('openremotewsl.explorer.deleteFolderHistoryItem', e => this.deleteDistroLocation(e)));
|
||||
this._register(vscode.commands.registerCommand('openremotewsl.explorer.setDefaultDistro', e => this.setDefaultDistro(e)));
|
||||
this._register(vscode.commands.registerCommand('openremotewsl.explorer.deleteDistro', e => this.deleteDistro(e)));
|
||||
}
|
||||
|
||||
getTreeItem(element: DataTreeItem): vscode.TreeItem {
|
||||
if (element instanceof DistroLocationItem) {
|
||||
const label = path.posix.basename(element.path).replace(/\.code-workspace$/, ' (Workspace)');
|
||||
const treeItem = new vscode.TreeItem(label);
|
||||
treeItem.description = path.posix.dirname(element.path);
|
||||
treeItem.iconPath = new vscode.ThemeIcon('folder');
|
||||
treeItem.contextValue = 'openremotewsl.explorer.folder';
|
||||
return treeItem;
|
||||
}
|
||||
|
||||
const treeItem = new vscode.TreeItem(element.name);
|
||||
treeItem.description = element.isDefault ? 'default distro' : undefined;
|
||||
treeItem.collapsibleState = element.locations.length ? vscode.TreeItemCollapsibleState.Collapsed : vscode.TreeItemCollapsibleState.None;
|
||||
treeItem.iconPath = new vscode.ThemeIcon('vm');
|
||||
treeItem.contextValue = 'openremotewsl.explorer.distro';
|
||||
return treeItem;
|
||||
}
|
||||
|
||||
async getChildren(element?: DistroItem): Promise<DataTreeItem[]> {
|
||||
if (!element) {
|
||||
const distros = await this.wslManager.listDistros();
|
||||
return distros.map(distro => new DistroItem(distro.name, distro.isDefault, this.locationHistory.getHistory(distro.name)));
|
||||
}
|
||||
if (element instanceof DistroItem) {
|
||||
return element.locations.map(location => new DistroLocationItem(location, element.name));
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
private refresh() {
|
||||
this._onDidChangeTreeData.fire();
|
||||
}
|
||||
|
||||
private async deleteDistroLocation(element: DistroLocationItem) {
|
||||
await this.locationHistory.removeLocation(element.name, element.path);
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
private async openRemoteWSLWindow(element: DistroItem, reuseWindow: boolean) {
|
||||
openRemoteWSLWindow(element.name, reuseWindow);
|
||||
}
|
||||
|
||||
private async openRemoteWSLocationWindow(element: DistroLocationItem, reuseWindow: boolean) {
|
||||
openRemoteWSLLocationWindow(element.name, element.path, reuseWindow);
|
||||
}
|
||||
|
||||
private async setDefaultDistro(element: DistroItem) {
|
||||
await setDefaultWSLDistro(this.wslManager, element.name);
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
private async deleteDistro(element: DistroItem) {
|
||||
await deleteWSLDistro(this.wslManager, element.name);
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import Log from './common/logger';
|
||||
import { RemoteWSLResolver, REMOTE_WSL_AUTHORITY } from './authResolver';
|
||||
import { promptOpenRemoteWSLWindow } from './commands';
|
||||
import { DistroTreeDataProvider } from './distroTreeView';
|
||||
import { getRemoteWorkspaceLocationData, RemoteLocationHistory } from './remoteLocationHistory';
|
||||
import { WSLManager } from './wsl/wslManager';
|
||||
import { isWindows } from './common/platform';
|
||||
|
||||
export async function activate(context: vscode.ExtensionContext) {
|
||||
if (!isWindows) {
|
||||
return;
|
||||
}
|
||||
|
||||
const logger = new Log('Remote - WSL');
|
||||
context.subscriptions.push(logger);
|
||||
|
||||
const wslManager = new WSLManager(logger);
|
||||
const remoteWSLResolver = new RemoteWSLResolver(wslManager, logger);
|
||||
context.subscriptions.push(vscode.workspace.registerRemoteAuthorityResolver(REMOTE_WSL_AUTHORITY, remoteWSLResolver));
|
||||
context.subscriptions.push(remoteWSLResolver);
|
||||
|
||||
const locationHistory = new RemoteLocationHistory(context);
|
||||
const locationData = getRemoteWorkspaceLocationData();
|
||||
if (locationData) {
|
||||
await locationHistory.addLocation(locationData[0], locationData[1]);
|
||||
}
|
||||
|
||||
const distroTreeDataProvider = new DistroTreeDataProvider(locationHistory, wslManager);
|
||||
context.subscriptions.push(vscode.window.createTreeView('wslTargets', { treeDataProvider: distroTreeDataProvider }));
|
||||
context.subscriptions.push(distroTreeDataProvider);
|
||||
|
||||
context.subscriptions.push(vscode.commands.registerCommand('openremotewsl.connect', () => promptOpenRemoteWSLWindow(wslManager, true, true)));
|
||||
context.subscriptions.push(vscode.commands.registerCommand('openremotewsl.connectInNewWindow', () => promptOpenRemoteWSLWindow(wslManager, true, false)));
|
||||
context.subscriptions.push(vscode.commands.registerCommand('openremotewsl.connectUsingDistro', () => promptOpenRemoteWSLWindow(wslManager, false, true)));
|
||||
context.subscriptions.push(vscode.commands.registerCommand('openremotewsl.connectUsingDistroInNewWindow', () => promptOpenRemoteWSLWindow(wslManager, false, false)));
|
||||
context.subscriptions.push(vscode.commands.registerCommand('openremotewsl.showLog', () => logger.show()));
|
||||
}
|
||||
|
||||
export function deactivate() {
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { REMOTE_WSL_AUTHORITY } from './authResolver';
|
||||
|
||||
export class RemoteLocationHistory {
|
||||
private static STORAGE_KEY = 'remoteLocationHistory_v0';
|
||||
|
||||
private remoteLocationHistory: Record<string, string[]> = {};
|
||||
|
||||
constructor(private context: vscode.ExtensionContext) {
|
||||
// context.globalState.update(RemoteLocationHistory.STORAGE_KEY, undefined);
|
||||
this.remoteLocationHistory = context.globalState.get(RemoteLocationHistory.STORAGE_KEY) || {};
|
||||
}
|
||||
|
||||
getHistory(host: string): string[] {
|
||||
return this.remoteLocationHistory[host] || [];
|
||||
}
|
||||
|
||||
async addLocation(host: string, path: string) {
|
||||
const hostLocations = this.remoteLocationHistory[host] || [];
|
||||
if (!hostLocations.includes(path)) {
|
||||
hostLocations.unshift(path);
|
||||
this.remoteLocationHistory[host] = hostLocations;
|
||||
|
||||
await this.context.globalState.update(RemoteLocationHistory.STORAGE_KEY, this.remoteLocationHistory);
|
||||
}
|
||||
}
|
||||
|
||||
async removeLocation(host: string, path: string) {
|
||||
let hostLocations = this.remoteLocationHistory[host] || [];
|
||||
hostLocations = hostLocations.filter(l => l !== path);
|
||||
this.remoteLocationHistory[host] = hostLocations;
|
||||
|
||||
await this.context.globalState.update(RemoteLocationHistory.STORAGE_KEY, this.remoteLocationHistory);
|
||||
}
|
||||
}
|
||||
|
||||
export function getRemoteWorkspaceLocationData(): [string, string] | undefined {
|
||||
let location = vscode.workspace.workspaceFile;
|
||||
if (location && location.scheme === 'vscode-remote' && location.authority.startsWith(REMOTE_WSL_AUTHORITY) && location.path.endsWith('.code-workspace')) {
|
||||
const [, distroName] = location.authority.split('+');
|
||||
return [distroName, location.path];
|
||||
}
|
||||
|
||||
location = vscode.workspace.workspaceFolders?.[0].uri;
|
||||
if (location && location.scheme === 'vscode-remote' && location.authority.startsWith(REMOTE_WSL_AUTHORITY)) {
|
||||
const [, distroName] = location.authority.split('+');
|
||||
return [distroName, location.path];
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
let vscodeProductJson: any;
|
||||
async function getVSCodeProductJson() {
|
||||
if (!vscodeProductJson) {
|
||||
const productJsonStr = await fs.promises.readFile(path.join(vscode.env.appRoot, 'product.json'), 'utf8');
|
||||
vscodeProductJson = JSON.parse(productJsonStr);
|
||||
}
|
||||
|
||||
return vscodeProductJson;
|
||||
}
|
||||
|
||||
export interface IServerConfig {
|
||||
version: string;
|
||||
commit: string;
|
||||
quality: string;
|
||||
release?: string; // void-like specific
|
||||
serverApplicationName: string;
|
||||
serverDataFolderName: string;
|
||||
serverDownloadUrlTemplate?: string; // void-like specific
|
||||
}
|
||||
|
||||
export async function getVSCodeServerConfig(): Promise<IServerConfig> {
|
||||
const productJson = await getVSCodeProductJson();
|
||||
|
||||
return {
|
||||
// version: vscode.version.replace('-insider', ''),
|
||||
commit: productJson.commit,
|
||||
quality: productJson.quality,
|
||||
release: productJson.release,
|
||||
serverApplicationName: productJson.serverApplicationName,
|
||||
serverDataFolderName: productJson.serverDataFolderName,
|
||||
serverDownloadUrlTemplate: productJson.serverDownloadUrlTemplate,
|
||||
// Void changed this
|
||||
version: productJson.voidVersion
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,353 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as crypto from 'crypto';
|
||||
import Log from './common/logger';
|
||||
import { getVSCodeServerConfig } from './serverConfig';
|
||||
import { WSLManager } from './wsl/wslManager';
|
||||
|
||||
export interface ServerInstallOptions {
|
||||
id: string;
|
||||
quality: string;
|
||||
commit: string;
|
||||
version: string;
|
||||
release?: string; // void specific
|
||||
extensionIds: string[];
|
||||
envVariables: string[];
|
||||
serverApplicationName: string;
|
||||
serverDataFolderName: string;
|
||||
serverDownloadUrlTemplate: string;
|
||||
}
|
||||
|
||||
export interface ServerInstallResult {
|
||||
exitCode: number;
|
||||
listeningOn: number;
|
||||
connectionToken: string;
|
||||
logFile: string;
|
||||
osReleaseId: string;
|
||||
arch: string;
|
||||
platform: string;
|
||||
tmpDir: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export class ServerInstallError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_DOWNLOAD_URL_TEMPLATE = 'https://github.com/voideditor/binaries/releases/download/${version}.${release}/void-reh-${os}-${arch}-${version}.${release}.tar.gz';
|
||||
|
||||
export async function installCodeServer(wslManager: WSLManager, distroName: string, serverDownloadUrlTemplate: string | undefined, extensionIds: string[], envVariables: string[], logger: Log): Promise<ServerInstallResult> {
|
||||
const scriptId = crypto.randomBytes(12).toString('hex');
|
||||
|
||||
const vscodeServerConfig = await getVSCodeServerConfig();
|
||||
const installOptions: ServerInstallOptions = {
|
||||
id: scriptId,
|
||||
version: vscodeServerConfig.version,
|
||||
commit: vscodeServerConfig.commit,
|
||||
quality: vscodeServerConfig.quality,
|
||||
release: vscodeServerConfig.release,
|
||||
extensionIds,
|
||||
envVariables,
|
||||
serverApplicationName: vscodeServerConfig.serverApplicationName,
|
||||
serverDataFolderName: vscodeServerConfig.serverDataFolderName,
|
||||
serverDownloadUrlTemplate: serverDownloadUrlTemplate ?? vscodeServerConfig.serverDownloadUrlTemplate ?? DEFAULT_DOWNLOAD_URL_TEMPLATE,
|
||||
};
|
||||
|
||||
const installServerScript = generateBashInstallScript(installOptions);
|
||||
|
||||
// Fish shell does not support heredoc so let's workaround it using -c option,
|
||||
// also replace single quotes (') within the script with ('\'') as there's no quoting within single quotes, see https://unix.stackexchange.com/a/24676
|
||||
const resp = await wslManager.exec('bash', ['-c', `'${installServerScript.replace(/'/g, `'\\''`)}'`], distroName);
|
||||
|
||||
const endScriptRegex = new RegExp(`${scriptId}: Server installation script done`, 'm');
|
||||
const commandOutput = await Promise.race([
|
||||
resp.exitPromise.then(result => ({ stdout: resp.stdout, stderr: resp.stderr, exitCode: result.exitCode })),
|
||||
new Promise<{ stdout: string; stderr: string; exitCode: number }>((resolve) => {
|
||||
resp.onStdoutData(buffer => {
|
||||
if (endScriptRegex.test(buffer.toString('utf8'))) {
|
||||
resolve({ stdout: resp.stdout, stderr: resp.stderr, exitCode: 0 });
|
||||
}
|
||||
});
|
||||
})
|
||||
]);
|
||||
|
||||
if (commandOutput.exitCode) {
|
||||
logger.trace('Server install command stderr:', commandOutput.stderr);
|
||||
}
|
||||
logger.trace('Server install command stdout:', commandOutput.stdout);
|
||||
|
||||
const resultMap = parseServerInstallOutput(commandOutput.stdout, scriptId);
|
||||
if (!resultMap) {
|
||||
throw new ServerInstallError(`Failed parsing install script output`);
|
||||
}
|
||||
|
||||
const exitCode = parseInt(resultMap.exitCode, 10);
|
||||
if (exitCode !== 0) {
|
||||
throw new ServerInstallError(`Couldn't install void server on remote server, install script returned non-zero exit status`);
|
||||
}
|
||||
|
||||
const listeningOn = parseInt(resultMap.listeningOn, 10);
|
||||
|
||||
const remoteEnvVars = Object.fromEntries(Object.entries(resultMap).filter(([key,]) => envVariables.includes(key)));
|
||||
|
||||
return {
|
||||
exitCode,
|
||||
listeningOn,
|
||||
connectionToken: resultMap.connectionToken,
|
||||
logFile: resultMap.logFile,
|
||||
osReleaseId: resultMap.osReleaseId,
|
||||
arch: resultMap.arch,
|
||||
platform: resultMap.platform,
|
||||
tmpDir: resultMap.tmpDir,
|
||||
...remoteEnvVars
|
||||
};
|
||||
}
|
||||
|
||||
function parseServerInstallOutput(str: string, scriptId: string): { [k: string]: string } | undefined {
|
||||
const startResultStr = `${scriptId}: start`;
|
||||
const endResultStr = `${scriptId}: end`;
|
||||
|
||||
const startResultIdx = str.indexOf(startResultStr);
|
||||
if (startResultIdx < 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const endResultIdx = str.indexOf(endResultStr, startResultIdx + startResultStr.length);
|
||||
if (endResultIdx < 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const installResult = str.substring(startResultIdx + startResultStr.length, endResultIdx);
|
||||
|
||||
const resultMap: { [k: string]: string } = {};
|
||||
const resultArr = installResult.split(/\r?\n/);
|
||||
for (const line of resultArr) {
|
||||
const [key, value] = line.split('==');
|
||||
resultMap[key] = value;
|
||||
}
|
||||
|
||||
return resultMap;
|
||||
}
|
||||
|
||||
function generateBashInstallScript({ id, quality, version, commit, release, extensionIds, envVariables, serverApplicationName, serverDataFolderName, serverDownloadUrlTemplate }: ServerInstallOptions) {
|
||||
const extensions = extensionIds.map(id => '--install-extension ' + id).join(' ');
|
||||
return `
|
||||
# Server installation script
|
||||
|
||||
TMP_DIR="\${XDG_RUNTIME_DIR:-"/tmp"}"
|
||||
|
||||
DISTRO_VERSION="${version}"
|
||||
DISTRO_COMMIT="${commit}"
|
||||
DISTRO_QUALITY="${quality}"
|
||||
DISTRO_VSCODIUM_RELEASE="${release ?? ''}"
|
||||
|
||||
SERVER_APP_NAME="${serverApplicationName}"
|
||||
SERVER_INITIAL_EXTENSIONS="${extensions}"
|
||||
SERVER_LISTEN_FLAG="--port=0"
|
||||
SERVER_DATA_DIR="$HOME/${serverDataFolderName}"
|
||||
SERVER_DIR="$SERVER_DATA_DIR/bin/$DISTRO_COMMIT"
|
||||
SERVER_SCRIPT="$SERVER_DIR/bin/$SERVER_APP_NAME"
|
||||
SERVER_LOGFILE="$SERVER_DATA_DIR/.$DISTRO_COMMIT.log"
|
||||
SERVER_PIDFILE="$SERVER_DATA_DIR/.$DISTRO_COMMIT.pid"
|
||||
SERVER_TOKENFILE="$SERVER_DATA_DIR/.$DISTRO_COMMIT.token"
|
||||
SERVER_OS=
|
||||
SERVER_ARCH=
|
||||
SERVER_CONNECTION_TOKEN=
|
||||
SERVER_DOWNLOAD_URL=
|
||||
|
||||
LISTENING_ON=
|
||||
OS_RELEASE_ID=
|
||||
ARCH=
|
||||
PLATFORM=
|
||||
|
||||
# Mimic output from logs of remote-ssh extension
|
||||
print_install_results_and_exit() {
|
||||
echo "${id}: start"
|
||||
echo "exitCode==$1=="
|
||||
echo "listeningOn==$LISTENING_ON=="
|
||||
echo "connectionToken==$SERVER_CONNECTION_TOKEN=="
|
||||
echo "logFile==$SERVER_LOGFILE=="
|
||||
echo "osReleaseId==$OS_RELEASE_ID=="
|
||||
echo "arch==$ARCH=="
|
||||
echo "platform==$PLATFORM=="
|
||||
echo "tmpDir==$TMP_DIR=="
|
||||
${envVariables.map(envVar => `echo "${envVar}==$${envVar}=="`).join('\n')}
|
||||
echo "${id}: end"
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Check if platform is supported
|
||||
PLATFORM="$(uname -s)"
|
||||
case $PLATFORM in
|
||||
Linux)
|
||||
SERVER_OS="linux"
|
||||
;;
|
||||
*)
|
||||
echo "Error platform not supported: $PLATFORM"
|
||||
print_install_results_and_exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Check machine architecture
|
||||
ARCH="$(uname -m)"
|
||||
case $ARCH in
|
||||
x86_64 | amd64)
|
||||
SERVER_ARCH="x64"
|
||||
;;
|
||||
armv7l | armv8l)
|
||||
SERVER_ARCH="armhf"
|
||||
;;
|
||||
arm64 | aarch64)
|
||||
SERVER_ARCH="arm64"
|
||||
;;
|
||||
*)
|
||||
echo "Error architecture not supported: $ARCH"
|
||||
print_install_results_and_exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# https://www.freedesktop.org/software/systemd/man/os-release.html
|
||||
OS_RELEASE_ID="$(grep -i '^ID=' /etc/os-release 2>/dev/null | sed 's/^ID=//gi' | sed 's/"//g')"
|
||||
if [[ -z $OS_RELEASE_ID ]]; then
|
||||
OS_RELEASE_ID="$(grep -i '^ID=' /usr/lib/os-release 2>/dev/null | sed 's/^ID=//gi' | sed 's/"//g')"
|
||||
if [[ -z $OS_RELEASE_ID ]]; then
|
||||
OS_RELEASE_ID="unknown"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Create installation folder
|
||||
if [[ ! -d $SERVER_DIR ]]; then
|
||||
mkdir -p $SERVER_DIR
|
||||
if (( $? > 0 )); then
|
||||
echo "Error creating server install directory"
|
||||
print_install_results_and_exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
SERVER_DOWNLOAD_URL="$(echo "${serverDownloadUrlTemplate.replace(/\$\{/g, '\\${')}" | sed "s/\\\${quality}/$DISTRO_QUALITY/g" | sed "s/\\\${version}/$DISTRO_VERSION/g" | sed "s/\\\${commit}/$DISTRO_COMMIT/g" | sed "s/\\\${os}/$SERVER_OS/g" | sed "s/\\\${arch}/$SERVER_ARCH/g" | sed "s/\\\${release}/$DISTRO_VSCODIUM_RELEASE/g")"
|
||||
|
||||
# Check if server script is already installed
|
||||
if [[ ! -f $SERVER_SCRIPT ]]; then
|
||||
if [[ "$SERVER_OS" = "dragonfly" ]] || [[ "$SERVER_OS" = "freebsd" ]]; then
|
||||
echo "Error "$SERVER_OS" needs manual installation of remote extension host"
|
||||
print_install_results_and_exit 1
|
||||
fi
|
||||
|
||||
pushd $SERVER_DIR > /dev/null
|
||||
|
||||
if [[ ! -z $(which wget) ]]; then
|
||||
wget --tries=3 --timeout=10 --continue --no-verbose -O vscode-server.tar.gz $SERVER_DOWNLOAD_URL
|
||||
elif [[ ! -z $(which curl) ]]; then
|
||||
curl --retry 3 --connect-timeout 10 --location --show-error --silent --output vscode-server.tar.gz $SERVER_DOWNLOAD_URL
|
||||
else
|
||||
echo "Error no tool to download server binary"
|
||||
print_install_results_and_exit 1
|
||||
fi
|
||||
|
||||
if (( $? > 0 )); then
|
||||
echo "Error downloading server from $SERVER_DOWNLOAD_URL"
|
||||
print_install_results_and_exit 1
|
||||
fi
|
||||
|
||||
tar -xf vscode-server.tar.gz --strip-components 1
|
||||
if (( $? > 0 )); then
|
||||
echo "Error while extracting server contents"
|
||||
print_install_results_and_exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f $SERVER_SCRIPT ]]; then
|
||||
echo "Error server contents are corrupted"
|
||||
print_install_results_and_exit 1
|
||||
fi
|
||||
|
||||
rm -f vscode-server.tar.gz
|
||||
|
||||
popd > /dev/null
|
||||
else
|
||||
echo "Server script already installed in $SERVER_SCRIPT"
|
||||
fi
|
||||
|
||||
# Try to find if server is already running
|
||||
if [[ -f $SERVER_PIDFILE ]]; then
|
||||
SERVER_PID="$(cat $SERVER_PIDFILE)"
|
||||
SERVER_RUNNING_PROCESS="$(ps -o pid,args -p $SERVER_PID | grep $SERVER_SCRIPT)"
|
||||
else
|
||||
SERVER_RUNNING_PROCESS="$(ps -o pid,args -A | grep $SERVER_SCRIPT | grep -v grep)"
|
||||
fi
|
||||
|
||||
if [[ -z $SERVER_RUNNING_PROCESS ]]; then
|
||||
if [[ -f $SERVER_LOGFILE ]]; then
|
||||
rm $SERVER_LOGFILE
|
||||
fi
|
||||
if [[ -f $SERVER_TOKENFILE ]]; then
|
||||
rm $SERVER_TOKENFILE
|
||||
fi
|
||||
|
||||
touch $SERVER_TOKENFILE
|
||||
chmod 600 $SERVER_TOKENFILE
|
||||
SERVER_CONNECTION_TOKEN="${crypto.randomUUID()}"
|
||||
echo $SERVER_CONNECTION_TOKEN > $SERVER_TOKENFILE
|
||||
|
||||
$SERVER_SCRIPT --start-server --host=127.0.0.1 $SERVER_LISTEN_FLAG $SERVER_INITIAL_EXTENSIONS --connection-token-file $SERVER_TOKENFILE --telemetry-level off --use-host-proxy --disable-websocket-compression --without-browser-env-var --enable-remote-auto-shutdown --accept-server-license-terms &> $SERVER_LOGFILE &
|
||||
echo $! > $SERVER_PIDFILE
|
||||
else
|
||||
echo "Server script is already running $SERVER_SCRIPT"
|
||||
fi
|
||||
|
||||
if [[ -f $SERVER_TOKENFILE ]]; then
|
||||
SERVER_CONNECTION_TOKEN="$(cat $SERVER_TOKENFILE)"
|
||||
else
|
||||
echo "Error server token file not found $SERVER_TOKENFILE"
|
||||
print_install_results_and_exit 1
|
||||
fi
|
||||
|
||||
if [[ -f $SERVER_LOGFILE ]]; then
|
||||
for i in {1..5}; do
|
||||
LISTENING_ON="$(cat $SERVER_LOGFILE | grep -E 'Extension host agent listening on .+' | sed 's/Extension host agent listening on //')"
|
||||
if [[ -n $LISTENING_ON ]]; then
|
||||
break
|
||||
fi
|
||||
sleep 0.5
|
||||
done
|
||||
|
||||
if [[ -z $LISTENING_ON ]]; then
|
||||
echo "Error server did not start sucessfully"
|
||||
print_install_results_and_exit 1
|
||||
fi
|
||||
else
|
||||
echo "Error server log file not found $SERVER_LOGFILE"
|
||||
print_install_results_and_exit 1
|
||||
fi
|
||||
|
||||
# Finish server setup and keep script running
|
||||
if [[ -z $SERVER_RUNNING_PROCESS ]]; then
|
||||
echo "${id}: start"
|
||||
echo "exitCode==0=="
|
||||
echo "listeningOn==$LISTENING_ON=="
|
||||
echo "connectionToken==$SERVER_CONNECTION_TOKEN=="
|
||||
echo "logFile==$SERVER_LOGFILE=="
|
||||
echo "osReleaseId==$OS_RELEASE_ID=="
|
||||
echo "arch==$ARCH=="
|
||||
echo "platform==$PLATFORM=="
|
||||
echo "tmpDir==$TMP_DIR=="
|
||||
${envVariables.map(envVar => `echo "${envVar}==$${envVar}=="`).join('\n')}
|
||||
echo "${id}: end"
|
||||
|
||||
echo "${id}: Server installation script done"
|
||||
|
||||
SERVER_PID="$(cat $SERVER_PIDFILE)"
|
||||
SERVER_RUNNING_PROCESS="$(ps -o pid,args -p $SERVER_PID | grep $SERVER_SCRIPT)"
|
||||
while [[ -n $SERVER_RUNNING_PROCESS ]]; do
|
||||
sleep 300;
|
||||
SERVER_RUNNING_PROCESS="$(ps -o pid,args -p $SERVER_PID | grep $SERVER_SCRIPT)"
|
||||
done
|
||||
else
|
||||
print_install_results_and_exit 0
|
||||
fi
|
||||
`;
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as cp from 'child_process';
|
||||
import Log from '../common/logger';
|
||||
import { EventEmitter } from '../common/event';
|
||||
|
||||
const wslBinary = 'wsl.exe';
|
||||
|
||||
export interface WSLDistro {
|
||||
isDefault: boolean;
|
||||
name: string;
|
||||
state: string;
|
||||
version: string;
|
||||
}
|
||||
|
||||
export interface WSLOnlineDistro {
|
||||
name: string;
|
||||
friendlyName: string;
|
||||
}
|
||||
|
||||
export class WSLManager {
|
||||
constructor(private readonly logger: Log) {
|
||||
}
|
||||
|
||||
async listDistros() {
|
||||
const resp = this._runWSLCommand(['--list', '--verbose'], 'utf16le');
|
||||
const { exitCode } = await resp.exitPromise;
|
||||
const { stdout, stderr } = resp;
|
||||
if (exitCode) {
|
||||
this.logger.trace(`Command wsl listDistros exited with code ${exitCode}`, stdout + '\n\n' + stderr);
|
||||
throw new Error(`Command wsl listDistros exited with code ${exitCode}`);
|
||||
}
|
||||
|
||||
const regex = /(?<default>\*|\s)\s+(?<name>[\w\.-]+)\s+(?<state>[\w]+)\s+(?<version>\d)/;
|
||||
const distros: WSLDistro[] = [];
|
||||
for (const line of stdout.split(/\r?\n/)) {
|
||||
const matches = line.match(regex);
|
||||
if (matches && matches.groups) {
|
||||
distros.push({
|
||||
isDefault: matches.groups.default === '*',
|
||||
name: matches.groups.name,
|
||||
state: matches.groups.state,
|
||||
version: matches.groups.version,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return distros;
|
||||
}
|
||||
|
||||
async listOnlineDistros() {
|
||||
const resp = this._runWSLCommand(['--list', '--online'], 'utf16le');
|
||||
const { exitCode } = await resp.exitPromise;
|
||||
const { stdout, stderr } = resp;
|
||||
if (exitCode) {
|
||||
this.logger.trace(`Command wsl listOnlineDistros exited with code ${exitCode}`, stdout + '\n\n' + stderr);
|
||||
throw new Error(`Command wsl listOnlineDistros exited with code ${exitCode}`);
|
||||
}
|
||||
|
||||
let lines = stdout.split(/\r?\n/);
|
||||
const idx = lines.findIndex(l => /\s*NAME\s+FRIENDLY NAME\s*/.test(l));
|
||||
lines = lines.slice(idx + 1);
|
||||
|
||||
const regex = /(?<name>[\w\.-]+)\s+(?<friendlyName>\w.+\w)/;
|
||||
const distros: WSLOnlineDistro[] = [];
|
||||
for (const line of lines) {
|
||||
const matches = line.match(regex);
|
||||
if (matches && matches.groups) {
|
||||
distros.push({
|
||||
name: matches.groups.name,
|
||||
friendlyName: matches.groups.friendlyName,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return distros;
|
||||
}
|
||||
|
||||
async setDefaultDistro(distroName: string) {
|
||||
const resp = this._runWSLCommand(['--set-default', distroName], 'utf16le');
|
||||
const { exitCode } = await resp.exitPromise;
|
||||
const { stdout, stderr } = resp;
|
||||
if (exitCode) {
|
||||
this.logger.trace(`Command wsl setDefaultDistro exited with code ${exitCode}`, stdout + '\n\n' + stderr);
|
||||
throw new Error(`Command wsl setDefaultDistro exited with code ${exitCode}`);
|
||||
}
|
||||
}
|
||||
|
||||
async deleteDistro(distroName: string) {
|
||||
const resp = this._runWSLCommand(['--unregister', distroName], 'utf16le');
|
||||
const { exitCode } = await resp.exitPromise;
|
||||
const { stdout, stderr } = resp;
|
||||
if (exitCode) {
|
||||
this.logger.trace(`Command wsl deleteDistro exited with code ${exitCode}`, stdout + '\n\n' + stderr);
|
||||
throw new Error(`Command wsl deleteDistro exited with code ${exitCode}`);
|
||||
}
|
||||
}
|
||||
|
||||
async exec(cmd: string, args: string[], distro: string) {
|
||||
return this._runWSLCommand(['--distribution', distro, '--', cmd, ...args], 'utf8');
|
||||
}
|
||||
|
||||
private _runWSLCommand(args: string[], encoding: 'utf8' | 'utf16le') {
|
||||
this.logger.trace(`Running WSL command: ${wslBinary} ${args.join(' ')}`);
|
||||
|
||||
const cmd = cp.spawn(wslBinary, args, { windowsHide: true, windowsVerbatimArguments: true });
|
||||
|
||||
const stdoutDataEmitter = new EventEmitter<Buffer>();
|
||||
const stdoutData: Buffer[] = [];
|
||||
const stderrDataEmitter = new EventEmitter<Buffer>();
|
||||
const stderrData: Buffer[] = [];
|
||||
cmd.stdout.on('data', (data: Buffer) => {
|
||||
stdoutData.push(data);
|
||||
stdoutDataEmitter.fire(data);
|
||||
});
|
||||
cmd.stderr.on('data', (data: Buffer) => {
|
||||
stderrData.push(data);
|
||||
stderrDataEmitter.fire(data);
|
||||
});
|
||||
|
||||
const exitPromise = new Promise<{ exitCode: number }>((resolve, reject) => {
|
||||
cmd.on('error', (err) => {
|
||||
this.logger.error(`Error running WSL command: ${wslBinary} ${args.join(' ')}`, err);
|
||||
reject(err);
|
||||
});
|
||||
cmd.on('exit', (code, _signal) => {
|
||||
resolve({ exitCode: code ?? 0 });
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
get stdout() {
|
||||
return Buffer.concat(stdoutData).toString(encoding);
|
||||
},
|
||||
get stderr() {
|
||||
return Buffer.concat(stderrData).toString(encoding);
|
||||
},
|
||||
get onStdoutData() {
|
||||
return stdoutDataEmitter.event;
|
||||
},
|
||||
get onStderrData() {
|
||||
return stderrDataEmitter.event;
|
||||
},
|
||||
exitPromise
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
class WSLTerminal {
|
||||
static NAME = 'WSL';
|
||||
|
||||
private getTerminal() {
|
||||
const wslTerminal = vscode.window.terminals.find(t => t.name === WSLTerminal.NAME);
|
||||
if (wslTerminal) {
|
||||
return wslTerminal;
|
||||
}
|
||||
return vscode.window.createTerminal(WSLTerminal.NAME);
|
||||
}
|
||||
|
||||
runCommand(command: string) {
|
||||
const wslTerminal = this.getTerminal();
|
||||
wslTerminal.show(false);
|
||||
wslTerminal.sendText(command, true);
|
||||
}
|
||||
}
|
||||
|
||||
export default new WSLTerminal();
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out",
|
||||
},
|
||||
"include": [
|
||||
"src/**/*",
|
||||
"../../src/vscode-dts/vscode.d.ts",
|
||||
"../../src/vscode-dts/vscode.proposed.resolvers.d.ts",
|
||||
"../../src/vscode-dts/vscode.proposed.contribViewsRemote.d.ts",
|
||||
]
|
||||
}
|
||||
Generated
+6419
-3939
File diff suppressed because it is too large
Load Diff
+7
-2
@@ -69,7 +69,7 @@
|
||||
"extensions-ci-pr": "node ./node_modules/gulp/bin/gulp.js extensions-ci-pr",
|
||||
"perf": "node scripts/code-perf.js",
|
||||
"update-build-ts-version": "npm install typescript@next && tsc -p ./build/tsconfig.build.json"
|
||||
},
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.39.0",
|
||||
"@floating-ui/react": "^0.27.5",
|
||||
@@ -77,6 +77,7 @@
|
||||
"@microsoft/1ds-core-js": "^3.2.13",
|
||||
"@microsoft/1ds-post-js": "^3.2.13",
|
||||
"@mistralai/mistralai": "^1.5.0",
|
||||
"@modelcontextprotocol/sdk": "^1.9.0",
|
||||
"@parcel/watcher": "2.5.1",
|
||||
"@types/semver": "^7.5.8",
|
||||
"@vscode/deviceid": "^0.1.1",
|
||||
@@ -102,6 +103,7 @@
|
||||
"@xterm/addon-webgl": "^0.19.0-beta.98",
|
||||
"@xterm/headless": "^5.6.0-beta.98",
|
||||
"@xterm/xterm": "^5.6.0-beta.98",
|
||||
"ajv": "^8.17.1",
|
||||
"cross-spawn": "^7.0.6",
|
||||
"diff": "^7.0.0",
|
||||
"eslint-plugin-react": "^7.37.4",
|
||||
@@ -109,6 +111,7 @@
|
||||
"http-proxy-agent": "^7.0.0",
|
||||
"https-proxy-agent": "^7.0.2",
|
||||
"jschardet": "3.1.4",
|
||||
"katex": "^0.16.22",
|
||||
"kerberos": "2.1.1",
|
||||
"lucide-react": "^0.477.0",
|
||||
"marked": "^15.0.7",
|
||||
@@ -123,6 +126,7 @@
|
||||
"posthog-node": "^4.8.1",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-tooltip": "^5.28.1",
|
||||
"tas-client-umd": "0.2.0",
|
||||
"v8-inspect-profiler": "^0.1.1",
|
||||
"vscode-html-languageservice": "^5.3.1",
|
||||
@@ -143,6 +147,7 @@
|
||||
"@types/eslint": "^9.6.1",
|
||||
"@types/gulp-svgmin": "^1.2.1",
|
||||
"@types/http-proxy-agent": "^2.0.1",
|
||||
"@types/katex": "^0.16.7",
|
||||
"@types/kerberos": "^1.1.2",
|
||||
"@types/minimist": "^1.2.1",
|
||||
"@types/mocha": "^9.1.1",
|
||||
@@ -178,7 +183,7 @@
|
||||
"cssnano": "^6.0.3",
|
||||
"debounce": "^1.0.0",
|
||||
"deemon": "^1.8.0",
|
||||
"electron": "34.2.0",
|
||||
"electron": "34.3.2",
|
||||
"eslint": "^9.11.1",
|
||||
"eslint-formatter-compact": "^8.40.0",
|
||||
"eslint-plugin-header": "3.1.1",
|
||||
|
||||
+6
-2
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"nameShort": "Void",
|
||||
"nameLong": "Void",
|
||||
"voidVersion": "1.0.2",
|
||||
"voidVersion": "1.2.6",
|
||||
"applicationName": "void",
|
||||
"dataFolderName": ".void-editor",
|
||||
"win32MutexName": "voideditor",
|
||||
@@ -35,5 +35,9 @@
|
||||
"serviceUrl": "https://marketplace.visualstudio.com/_apis/public/gallery",
|
||||
"itemUrl": "https://marketplace.visualstudio.com/items"
|
||||
},
|
||||
"builtInExtensions": []
|
||||
"builtInExtensions": [],
|
||||
"linkProtectionTrustedDomains": [
|
||||
"https://voideditor.com",
|
||||
"https://voideditor.dev"
|
||||
]
|
||||
}
|
||||
|
||||
+1231
-1039
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 173 KiB After Width: | Height: | Size: 200 KiB |
@@ -34,11 +34,8 @@ mkdir -pv "$APP_DIR/usr/share/icons/hicolor/256x256/apps"
|
||||
|
||||
# Exclude create-appimage.sh and appimagetool-x86_64.AppImage from being copied
|
||||
echo "Copying files excluding create-appimage.sh and appimagetool-x86_64.AppImage..."
|
||||
for file in ./*; do
|
||||
if [[ "$file" != "./create-appimage.sh" && "$file" != "./appimagetool-x86_64.AppImage" ]]; then
|
||||
cp -rv "$file" "$APP_DIR/usr/bin/"
|
||||
fi
|
||||
done
|
||||
|
||||
cp -v ./void "$APP_DIR/usr/bin/"
|
||||
|
||||
# Copy the icon to required locations
|
||||
cp -v ./void.png "$APP_DIR/void.png"
|
||||
@@ -1,3 +1,16 @@
|
||||
|
||||
|
||||
# README
|
||||
|
||||
This is a community-made AppImage creation script.
|
||||
|
||||
There are some reported bugs with it.
|
||||
|
||||
To generate an AppImage yourself, feel free to look at
|
||||
stable-linux.yml in the separate `void-builder/` repo,
|
||||
which runs a GitHub Action that builds the AppImage you see on our website.
|
||||
|
||||
|
||||
# Void AppImage Creation Script
|
||||
|
||||
This script automates the process of creating an AppImage for the Void Editor using Docker. It works on macOS and Linux platforms.
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// This bootstrap-fork module handles the initialization of a forked process in VS Code.
|
||||
// It sets up logging, exception handling, and loads the ESM module system.
|
||||
|
||||
import * as performance from './vs/base/common/performance.js';
|
||||
import { removeGlobalNodeJsModuleLookupPaths, devInjectNodeModuleLookupPath } from './bootstrap-node.js';
|
||||
import { bootstrapESM } from './bootstrap-esm.js';
|
||||
|
||||
+14
@@ -6,6 +6,7 @@
|
||||
import * as path from 'path';
|
||||
import * as fs from 'original-fs';
|
||||
import * as os from 'os';
|
||||
import { createRequire } from 'node:module';
|
||||
import { performance } from 'perf_hooks';
|
||||
import { configurePortable } from './bootstrap-node.js';
|
||||
import { bootstrapESM } from './bootstrap-esm.js';
|
||||
@@ -22,6 +23,7 @@ import { INLSConfiguration } from './vs/nls.js';
|
||||
import { NativeParsedArgs } from './vs/platform/environment/common/argv.js';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
perf.mark('code/didStartMain');
|
||||
|
||||
@@ -110,6 +112,17 @@ protocol.registerSchemesAsPrivileged([
|
||||
// Global app listeners
|
||||
registerListeners();
|
||||
|
||||
function resolveUserProduct() {
|
||||
const userProductPath = path.join(userDataPath, 'product.json');
|
||||
try {
|
||||
// Assign the product configuration to the global scope
|
||||
const productJson = require(userProductPath);
|
||||
// @ts-expect-error
|
||||
globalThis._VSCODE_USER_PRODUCT_JSON = productJson;
|
||||
} catch (ex) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We can resolve the NLS configuration early if it is defined
|
||||
* in argv.json before `app.ready` event. Otherwise we can only
|
||||
@@ -206,6 +219,7 @@ async function onReady() {
|
||||
async function startup(codeCachePath: string | undefined, nlsConfig: INLSConfiguration): Promise<void> {
|
||||
process.env['VSCODE_NLS_CONFIG'] = JSON.stringify(nlsConfig);
|
||||
process.env['VSCODE_CODE_CACHE_PATH'] = codeCachePath || '';
|
||||
resolveUserProduct();
|
||||
|
||||
// Bootstrap ESM
|
||||
await bootstrapESM();
|
||||
|
||||
@@ -56,7 +56,8 @@ export type ExtensionVirtualWorkspaceSupport = {
|
||||
|
||||
export interface IProductConfiguration {
|
||||
readonly version: string;
|
||||
readonly voidVersion?: string; // Void added this
|
||||
readonly voidVersion: string; // Void added this
|
||||
readonly release: string; // VSCodium added this
|
||||
readonly date?: string;
|
||||
readonly quality?: string;
|
||||
readonly commit?: string;
|
||||
|
||||
@@ -121,6 +121,93 @@ export class SmartSelectController implements IEditorContribution {
|
||||
}
|
||||
this._state = this._state.map(state => state.mov(forward));
|
||||
const newSelections = this._state.map(state => Selection.fromPositions(state.ranges[state.index].getStartPosition(), state.ranges[state.index].getEndPosition()));
|
||||
|
||||
// Void changed this to skip over added whitespace when using smartSelect
|
||||
// // Store the original selections for comparison
|
||||
// const originalSelections = selections;
|
||||
|
||||
// // Keep skipping while we're only adding/removing whitespace
|
||||
// let keepSkipping = true;
|
||||
// let skipCount = 0;
|
||||
// const MAX_SKIPS = 5; // Avoid infinite loops by setting a reasonable limit
|
||||
|
||||
// while (keepSkipping && skipCount < MAX_SKIPS) {
|
||||
// keepSkipping = false; // Reset for each iteration
|
||||
|
||||
// // Check if all selections only added/removed whitespace
|
||||
// if (originalSelections.length === newSelections.length) {
|
||||
// for (let i = 0; i < originalSelections.length; i++) {
|
||||
// const oldSel = originalSelections[i];
|
||||
// const newSel = newSelections[i];
|
||||
|
||||
// if (forward) { // For expanding (^+Shift+Right)
|
||||
// // Skip if only whitespace was added
|
||||
// const oldText = model.getValueInRange(oldSel).trim();
|
||||
// const newText = model.getValueInRange(newSel).trim();
|
||||
// const onlyWhitespaceAdded = oldText === newText && oldText.length > 0;
|
||||
|
||||
// if (onlyWhitespaceAdded) {
|
||||
// console.log(`SMART SELECT - SKIPPING (EXPAND) [${skipCount + 1}]:`, {
|
||||
// reason: 'only whitespace added',
|
||||
// oldText: model.getValueInRange(oldSel),
|
||||
// newText: model.getValueInRange(newSel)
|
||||
// });
|
||||
// keepSkipping = true;
|
||||
// break;
|
||||
// }
|
||||
// } else { // For shrinking (^+Shift+Left)
|
||||
// // Skip if only whitespace was removed
|
||||
// const oldText = model.getValueInRange(oldSel).trim();
|
||||
// const newText = model.getValueInRange(newSel).trim();
|
||||
// const onlyWhitespaceRemoved = oldText === newText && newText.length > 0;
|
||||
|
||||
// if (onlyWhitespaceRemoved) {
|
||||
// console.log(`SMART SELECT - SKIPPING (SHRINK) [${skipCount + 1}]:`, {
|
||||
// reason: 'only whitespace removed',
|
||||
// oldText: model.getValueInRange(oldSel),
|
||||
// newText: model.getValueInRange(newSel)
|
||||
// });
|
||||
// keepSkipping = true;
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// // If we need to skip, move one more time
|
||||
// if (keepSkipping) {
|
||||
// skipCount++;
|
||||
|
||||
// // Try to move to the next range
|
||||
// const prevState = this._state;
|
||||
// this._state = this._state.map(state => state.mov(forward));
|
||||
|
||||
// // Check if we've reached the end of available ranges
|
||||
// const stateUnchanged = this._state.every((state, idx) =>
|
||||
// state.index === prevState[idx].index
|
||||
// );
|
||||
|
||||
// if (stateUnchanged) {
|
||||
// // We can't move any further, so stop skipping
|
||||
// keepSkipping = false;
|
||||
// } else {
|
||||
// // Update selections for the next iteration
|
||||
// newSelections = this._state.map(state => Selection.fromPositions(
|
||||
// state.ranges[state.index].getStartPosition(),
|
||||
// state.ranges[state.index].getEndPosition()
|
||||
// ));
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// // Print AFTER selection (before actually setting it)
|
||||
// console.log('SMART SELECT - AFTER:', newSelections.map(s => {
|
||||
// return {
|
||||
// range: `(${s.startLineNumber},${s.startColumn}) -> (${s.endLineNumber},${s.endColumn})`,
|
||||
// text: model.getValueInRange(s)
|
||||
// };
|
||||
// }));
|
||||
|
||||
this._ignoreSelection = true;
|
||||
try {
|
||||
this._editor.setSelections(newSelections);
|
||||
|
||||
@@ -230,7 +230,7 @@ export class DiagnosticsService implements IDiagnosticsService {
|
||||
|
||||
private formatEnvironment(info: IMainProcessDiagnostics): string {
|
||||
const output: string[] = [];
|
||||
output.push(`Version: ${this.productService.nameShort} ${this.productService.version} (${this.productService.commit || 'Commit unknown'}, ${this.productService.date || 'Date unknown'})`);
|
||||
output.push(`Version: ${this.productService.nameShort} ${this.productService.version} ${this.productService.release || 'Release unknown'} (${this.productService.commit || 'Commit unknown'}, ${this.productService.date || 'Date unknown'})`);
|
||||
output.push(`OS Version: ${osLib.type()} ${osLib.arch()} ${osLib.release()}`);
|
||||
const cpus = osLib.cpus();
|
||||
if (cpus && cpus.length > 0) {
|
||||
|
||||
@@ -52,7 +52,6 @@ import { IProductService } from '../../product/common/productService.js';
|
||||
import { ITelemetryService } from '../../telemetry/common/telemetry.js';
|
||||
import { IUriIdentityService } from '../../uriIdentity/common/uriIdentity.js';
|
||||
import { IUserDataProfilesService } from '../../userDataProfile/common/userDataProfile.js';
|
||||
import { IConfigurationService } from '../../configuration/common/configuration.js';
|
||||
import { isLinux } from '../../../base/common/platform.js';
|
||||
|
||||
export const INativeServerExtensionManagementService = refineServiceDecorator<IExtensionManagementService, INativeServerExtensionManagementService>(IExtensionManagementService);
|
||||
@@ -85,7 +84,6 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi
|
||||
@IDownloadService private downloadService: IDownloadService,
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
@IFileService private readonly fileService: IFileService,
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService,
|
||||
@IProductService productService: IProductService,
|
||||
@IAllowedExtensionsService allowedExtensionsService: IAllowedExtensionsService,
|
||||
@IUriIdentityService uriIdentityService: IUriIdentityService,
|
||||
@@ -324,8 +322,7 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi
|
||||
|
||||
private async downloadExtension(extension: IGalleryExtension, operation: InstallOperation, verifySignature: boolean, clientTargetPlatform?: TargetPlatform): Promise<{ readonly location: URI; readonly verificationStatus: ExtensionSignatureVerificationCode | undefined }> {
|
||||
if (verifySignature) {
|
||||
const value = this.configurationService.getValue('extensions.verifySignature');
|
||||
verifySignature = isBoolean(value) ? value : true;
|
||||
verifySignature = false; // Void disable-signature-verification-patch
|
||||
}
|
||||
const { location, verificationStatus } = await this.extensionsDownloader.download(extension, operation, verifySignature, clientTargetPlatform);
|
||||
|
||||
|
||||
@@ -404,7 +404,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
|
||||
}
|
||||
|
||||
private async getShellCommandLink(): Promise<{ readonly source: string; readonly target: string }> {
|
||||
const target = resolve(this.environmentMainService.appRoot, 'bin', 'code');
|
||||
const target = resolve(this.environmentMainService.appRoot, 'bin', this.productService.applicationName);
|
||||
const source = `/usr/local/bin/${this.productService.applicationName}`;
|
||||
|
||||
// Ensure source exists
|
||||
@@ -639,7 +639,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
|
||||
|
||||
// macOS
|
||||
if (this.environmentMainService.isBuilt) {
|
||||
return join(this.environmentMainService.appRoot, 'bin', 'code');
|
||||
return join(this.environmentMainService.appRoot, 'bin', `${this.productService.applicationName}`);
|
||||
}
|
||||
|
||||
return join(this.environmentMainService.appRoot, 'scripts', 'code-cli.sh');
|
||||
|
||||
@@ -43,10 +43,11 @@ else if (globalThis._VSCODE_PRODUCT_JSON && globalThis._VSCODE_PACKAGE_JSON) {
|
||||
// want to have it running out of sources so we
|
||||
// read it from package.json only when we need it.
|
||||
if (!product.version) {
|
||||
const pkg = globalThis._VSCODE_PACKAGE_JSON as { version: string };
|
||||
const pkg = globalThis._VSCODE_PACKAGE_JSON as { version: string, release: string };
|
||||
|
||||
Object.assign(product, {
|
||||
version: pkg.version
|
||||
version: pkg.version,
|
||||
release: pkg.release
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,8 @@ export const enum StateType {
|
||||
export const enum UpdateType {
|
||||
Setup,
|
||||
Archive,
|
||||
Snap
|
||||
Snap,
|
||||
WindowsInstaller
|
||||
}
|
||||
|
||||
export const enum DisablementReason {
|
||||
@@ -108,3 +109,41 @@ export interface IUpdateService {
|
||||
isLatestVersion(): Promise<boolean | undefined>;
|
||||
_applySpecificUpdate(packagePath: string): Promise<void>;
|
||||
}
|
||||
|
||||
|
||||
export type Architecture =
|
||||
| 'arm'
|
||||
| 'arm64'
|
||||
| 'ia32'
|
||||
| 'loong64'
|
||||
| 'mips'
|
||||
| 'mipsel'
|
||||
| 'ppc'
|
||||
| 'ppc64'
|
||||
| 'riscv64'
|
||||
| 's390'
|
||||
| 's390x'
|
||||
| 'x64';
|
||||
|
||||
export type Platform =
|
||||
| 'aix'
|
||||
| 'android'
|
||||
| 'darwin'
|
||||
| 'freebsd'
|
||||
| 'haiku'
|
||||
| 'linux'
|
||||
| 'openbsd'
|
||||
| 'sunos'
|
||||
| 'win32'
|
||||
| 'cygwin'
|
||||
| 'netbsd';
|
||||
|
||||
export type Quality =
|
||||
| 'insider'
|
||||
| 'stable';
|
||||
|
||||
export type Target =
|
||||
| 'archive'
|
||||
| 'msi'
|
||||
| 'system'
|
||||
| 'user';
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// import { timeout } from '../../../base/common/async.js';
|
||||
import { timeout } from '../../../base/common/async.js';
|
||||
import { CancellationToken } from '../../../base/common/cancellation.js';
|
||||
import { Emitter, Event } from '../../../base/common/event.js';
|
||||
import { IConfigurationService } from '../../configuration/common/configuration.js';
|
||||
@@ -13,13 +14,16 @@ import { ILifecycleMainService, LifecycleMainPhase } from '../../lifecycle/elect
|
||||
import { ILogService } from '../../log/common/log.js';
|
||||
import { IProductService } from '../../product/common/productService.js';
|
||||
import { IRequestService } from '../../request/common/request.js';
|
||||
import { AvailableForDownload, DisablementReason, IUpdateService, State, StateType, UpdateType } from '../common/update.js';
|
||||
import { Architecture, AvailableForDownload, DisablementReason, IUpdateService, Platform, State, StateType, Target, UpdateType } from '../common/update.js';
|
||||
|
||||
export function createUpdateURL(platform: string, quality: string, productService: IProductService): string {
|
||||
// return `https://voideditor.dev/api/update/${platform}/stable`;
|
||||
// return `${productService.updateUrl}/api/update/${platform}/${quality}/${productService.commit}`;
|
||||
// https://github.com/VSCodium/update-api
|
||||
return `https://updates.voideditor.dev/api/update/${platform}/${quality}/${productService.commit}`;
|
||||
// Void - VSCodium's version-1-update.patch
|
||||
export function createUpdateURL(productService: IProductService, quality: string, platform: Platform, architecture: Architecture, target?: Target): string { // return `https://voideditor.dev/api/update/${platform}/stable`;
|
||||
if (target) {
|
||||
return `${productService.updateUrl}/${quality}/${platform}/${architecture}/${target}/latest.json`;
|
||||
} else { // we shouldn't usually have a target:
|
||||
// https://raw.githubusercontent.com/voideditor/versions/refs/heads/main/stable/darwin/arm64/latest.json
|
||||
return `${productService.updateUrl}/${quality}/${platform}/${architecture}/latest.json`;
|
||||
}
|
||||
}
|
||||
|
||||
export type UpdateErrorClassification = {
|
||||
@@ -81,22 +85,20 @@ export abstract class AbstractUpdateService implements IUpdateService {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState(State.Disabled(DisablementReason.ManuallyDisabled));
|
||||
// Void - re-enabled auto updates
|
||||
// this.setState(State.Disabled(DisablementReason.ManuallyDisabled));
|
||||
|
||||
|
||||
// Void - temporarily disabled while we figure out how to do this the right way
|
||||
|
||||
// this.setState(State.Idle(this.getUpdateType()));
|
||||
this.setState(State.Idle(this.getUpdateType()));
|
||||
|
||||
// start checking for updates after 10 seconds
|
||||
// this.scheduleCheckForUpdates(10 * 1000).then(undefined, err => this.logService.error(err));
|
||||
this.scheduleCheckForUpdates(10 * 1000).then(undefined, err => this.logService.error(err));
|
||||
}
|
||||
|
||||
// private async scheduleCheckForUpdates(delay = 60 * 60 * 1000): Promise<void> {
|
||||
// await timeout(delay);
|
||||
// await this.checkForUpdates(false);
|
||||
// return await this.scheduleCheckForUpdates(60 * 60 * 1000);
|
||||
// }
|
||||
private async scheduleCheckForUpdates(delay = 60 * 60 * 1000): Promise<void> {
|
||||
await timeout(delay);
|
||||
await this.checkForUpdates(false);
|
||||
return await this.scheduleCheckForUpdates(60 * 60 * 1000);
|
||||
}
|
||||
|
||||
async checkForUpdates(explicit: boolean): Promise<void> {
|
||||
this.logService.trace('update#checkForUpdates, state = ', this.state.type);
|
||||
|
||||
@@ -13,11 +13,12 @@ import { IEnvironmentMainService } from '../../environment/electron-main/environ
|
||||
import { ILifecycleMainService, IRelaunchHandler, IRelaunchOptions } from '../../lifecycle/electron-main/lifecycleMainService.js';
|
||||
import { ILogService } from '../../log/common/log.js';
|
||||
import { IProductService } from '../../product/common/productService.js';
|
||||
import { IRequestService } from '../../request/common/request.js';
|
||||
import { IRequestService, asJson } from '../../request/common/request.js';
|
||||
import { ITelemetryService } from '../../telemetry/common/telemetry.js';
|
||||
import { IUpdate, State, StateType, UpdateType } from '../common/update.js';
|
||||
import { AbstractUpdateService, createUpdateURL, UpdateErrorClassification } from './abstractUpdateService.js';
|
||||
|
||||
import { CancellationToken } from '../../../base/common/cancellation.js';
|
||||
import * as semver from 'semver';
|
||||
export class DarwinUpdateService extends AbstractUpdateService implements IRelaunchHandler {
|
||||
|
||||
private readonly disposables = new DisposableStore();
|
||||
@@ -75,13 +76,7 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau
|
||||
|
||||
// Void: buildUpdateFeedUrl -> doBuildUpdateFeedUrl
|
||||
protected doBuildUpdateFeedUrl(quality: string): string | undefined {
|
||||
let assetID: string;
|
||||
if (!this.productService.darwinUniversalAssetId) {
|
||||
assetID = process.arch === 'x64' ? 'darwin' : 'darwin-arm64';
|
||||
} else {
|
||||
assetID = this.productService.darwinUniversalAssetId;
|
||||
}
|
||||
const url = createUpdateURL(assetID, quality, this.productService);
|
||||
const url = createUpdateURL(this.productService, quality, process.platform, process.arch);
|
||||
try {
|
||||
electron.autoUpdater.setFeedURL({ url });
|
||||
} catch (e) {
|
||||
@@ -93,8 +88,36 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau
|
||||
}
|
||||
|
||||
protected doCheckForUpdates(context: any): void {
|
||||
if (!this.url) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState(State.CheckingForUpdates(context));
|
||||
electron.autoUpdater.checkForUpdates();
|
||||
// electron.autoUpdater.checkForUpdates();
|
||||
|
||||
this.requestService.request({ url: this.url }, CancellationToken.None)
|
||||
.then<IUpdate | null>(asJson)
|
||||
.then(update => {
|
||||
if (!update || !update.url || !update.version || !update.productVersion) {
|
||||
this.setState(State.Idle(UpdateType.Setup));
|
||||
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
const fetchedVersion = update.productVersion.replace(/(\d+\.\d+\.\d+)(?:\.(\d+))(\-\w+)?/, '$1$3+$2');
|
||||
const currentVersion = `${this.productService.voidVersion}+${this.productService.release}`;
|
||||
// Void compares voidVersion, not VSCode version
|
||||
// const currentVersion = `${this.productService.version}+${this.productService.release}`;
|
||||
|
||||
if (semver.compareBuild(currentVersion, fetchedVersion) >= 0) {
|
||||
this.setState(State.Idle(UpdateType.Setup));
|
||||
}
|
||||
else {
|
||||
electron.autoUpdater.checkForUpdates();
|
||||
}
|
||||
|
||||
return Promise.resolve(null);
|
||||
})
|
||||
}
|
||||
|
||||
private onUpdateAvailable(): void {
|
||||
|
||||
@@ -30,7 +30,7 @@ export class LinuxUpdateService extends AbstractUpdateService {
|
||||
|
||||
// Void: buildUpdateFeedUrl -> doBuildUpdateFeedUrl
|
||||
protected doBuildUpdateFeedUrl(quality: string): string {
|
||||
return createUpdateURL(`linux-${process.arch}`, quality, this.productService);
|
||||
return createUpdateURL(this.productService, quality, process.platform, process.arch);
|
||||
}
|
||||
|
||||
protected doCheckForUpdates(context: any): void {
|
||||
|
||||
@@ -9,7 +9,6 @@ import { tmpdir } from 'os';
|
||||
import { timeout } from '../../../base/common/async.js';
|
||||
import { CancellationToken } from '../../../base/common/cancellation.js';
|
||||
import { memoize } from '../../../base/common/decorators.js';
|
||||
import { hash } from '../../../base/common/hash.js';
|
||||
import * as path from '../../../base/common/path.js';
|
||||
import { URI } from '../../../base/common/uri.js';
|
||||
import { checksum } from '../../../base/node/crypto.js';
|
||||
@@ -23,8 +22,9 @@ import { INativeHostMainService } from '../../native/electron-main/nativeHostMai
|
||||
import { IProductService } from '../../product/common/productService.js';
|
||||
import { asJson, IRequestService } from '../../request/common/request.js';
|
||||
import { ITelemetryService } from '../../telemetry/common/telemetry.js';
|
||||
import { AvailableForDownload, DisablementReason, IUpdate, State, StateType, UpdateType } from '../common/update.js';
|
||||
import { AbstractUpdateService, createUpdateURL, UpdateErrorClassification } from './abstractUpdateService.js';
|
||||
import { AvailableForDownload, DisablementReason, IUpdate, State, StateType, Target, UpdateType } from '../common/update.js';
|
||||
import { AbstractUpdateService, createUpdateURL } from './abstractUpdateService.js';
|
||||
import * as semver from 'semver';
|
||||
|
||||
async function pollUntil(fn: () => boolean, millis = 1000): Promise<void> {
|
||||
while (!fn()) {
|
||||
@@ -40,9 +40,13 @@ interface IAvailableUpdate {
|
||||
let _updateType: UpdateType | undefined = undefined;
|
||||
function getUpdateType(): UpdateType {
|
||||
if (typeof _updateType === 'undefined') {
|
||||
_updateType = fs.existsSync(path.join(path.dirname(process.execPath), 'unins000.exe'))
|
||||
? UpdateType.Setup
|
||||
: UpdateType.Archive;
|
||||
if (fs.existsSync(path.join(path.dirname(process.execPath), 'unins000.exe'))) {
|
||||
_updateType = UpdateType.Setup;
|
||||
} else if (path.basename(path.normalize(path.join(process.execPath, '..', '..'))) === 'Program Files') {
|
||||
_updateType = UpdateType.WindowsInstaller;
|
||||
} else {
|
||||
_updateType = UpdateType.Archive;
|
||||
}
|
||||
}
|
||||
|
||||
return _updateType;
|
||||
@@ -61,6 +65,7 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun
|
||||
constructor(
|
||||
@ILifecycleMainService lifecycleMainService: ILifecycleMainService,
|
||||
@IConfigurationService configurationService: IConfigurationService,
|
||||
// @ts-expect-error
|
||||
@ITelemetryService private readonly telemetryService: ITelemetryService,
|
||||
@IEnvironmentMainService environmentMainService: IEnvironmentMainService,
|
||||
@IRequestService requestService: IRequestService,
|
||||
@@ -101,15 +106,24 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun
|
||||
|
||||
// Void: buildUpdateFeedUrl -> doBuildUpdateFeedUrl
|
||||
protected doBuildUpdateFeedUrl(quality: string): string | undefined {
|
||||
let platform = `win32-${process.arch}`;
|
||||
|
||||
if (getUpdateType() === UpdateType.Archive) {
|
||||
platform += '-archive';
|
||||
} else if (this.productService.target === 'user') {
|
||||
platform += '-user';
|
||||
let target: Target;
|
||||
switch (getUpdateType()) {
|
||||
case UpdateType.Archive:
|
||||
target = 'archive'
|
||||
break;
|
||||
case UpdateType.WindowsInstaller:
|
||||
target = 'msi'
|
||||
break;
|
||||
default:
|
||||
if (this.productService.target === 'user') {
|
||||
target = 'user'
|
||||
}
|
||||
else {
|
||||
target = 'system'
|
||||
}
|
||||
}
|
||||
|
||||
return createUpdateURL(platform, quality, this.productService);
|
||||
return createUpdateURL(this.productService, quality, process.platform, process.arch, target);
|
||||
}
|
||||
|
||||
protected doCheckForUpdates(context: any): void {
|
||||
@@ -129,6 +143,16 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
const fetchedVersion = update.productVersion.replace(/(\d+\.\d+\.\d+)(?:\.(\d+))(\-\w+)?/, '$1$3+$2');
|
||||
const currentVersion = `${this.productService.voidVersion}+${this.productService.release}`;
|
||||
// Void compares voidVersion, not VSCode version
|
||||
// const currentVersion = `${this.productService.version}+${this.productService.release}`;
|
||||
|
||||
if (semver.compareBuild(currentVersion, fetchedVersion) >= 0) {
|
||||
this.setState(State.Idle(updateType));
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
if (updateType === UpdateType.Archive) {
|
||||
this.setState(State.AvailableForDownload(update));
|
||||
return Promise.resolve(null);
|
||||
@@ -155,7 +179,7 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun
|
||||
this.availableUpdate = { packagePath };
|
||||
this.setState(State.Downloaded(update));
|
||||
|
||||
const fastUpdatesEnabled = this.configurationService.getValue('update.enableWindowsBackgroundUpdates');
|
||||
const fastUpdatesEnabled = getUpdateType() === UpdateType.Setup && this.configurationService.getValue('update.enableWindowsBackgroundUpdates');
|
||||
if (fastUpdatesEnabled) {
|
||||
if (this.productService.target === 'user') {
|
||||
this.doApplyUpdate();
|
||||
@@ -167,7 +191,6 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun
|
||||
});
|
||||
})
|
||||
.then(undefined, err => {
|
||||
this.telemetryService.publicLog2<{ messageHash: string }, UpdateErrorClassification>('update:error', { messageHash: String(hash(String(err))) });
|
||||
this.logService.error(err);
|
||||
|
||||
// only show message when explicitly checking for updates
|
||||
@@ -251,10 +274,18 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun
|
||||
if (this.availableUpdate.updateFilePath) {
|
||||
fs.unlinkSync(this.availableUpdate.updateFilePath);
|
||||
} else {
|
||||
spawn(this.availableUpdate.packagePath, ['/silent', '/log', '/mergetasks=runcode,!desktopicon,!quicklaunchicon'], {
|
||||
detached: true,
|
||||
stdio: ['ignore', 'ignore', 'ignore']
|
||||
});
|
||||
const type = getUpdateType();
|
||||
if (type === UpdateType.WindowsInstaller) {
|
||||
spawn('msiexec.exe', ['/i', this.availableUpdate.packagePath], {
|
||||
detached: true,
|
||||
stdio: ['ignore', 'ignore', 'ignore']
|
||||
});
|
||||
} else {
|
||||
spawn(this.availableUpdate.packagePath, ['/silent', '/log', '/mergetasks=runcode,!desktopicon,!quicklaunchicon'], {
|
||||
detached: true,
|
||||
stdio: ['ignore', 'ignore', 'ignore']
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ export class AuxiliaryBarPart extends AbstractPaneCompositePart {
|
||||
static readonly viewContainersWorkspaceStateKey = 'workbench.auxiliarybar.viewContainersWorkspaceState';
|
||||
|
||||
// Use the side bar dimensions
|
||||
override readonly minimumWidth: number = 230; // Void changed this (was 170)
|
||||
override readonly minimumWidth: number = 280; // Void changed this (was 170)
|
||||
override readonly maximumWidth: number = Number.POSITIVE_INFINITY;
|
||||
override readonly minimumHeight: number = 0;
|
||||
override readonly maximumHeight: number = Number.POSITIVE_INFINITY;
|
||||
|
||||
@@ -21,6 +21,7 @@ import { MarkdownRenderer, openLinkFromMarkdown } from '../../../../editor/brows
|
||||
import { defaultButtonStyles, defaultCheckboxStyles, defaultDialogStyles, defaultInputBoxStyles } from '../../../../platform/theme/browser/defaultStyles.js';
|
||||
import { ResultKind } from '../../../../platform/keybinding/common/keybindingResolver.js';
|
||||
import { IOpenerService } from '../../../../platform/opener/common/opener.js';
|
||||
import { getReleaseString } from '../../../../workbench/common/release.js';
|
||||
|
||||
export class BrowserDialogHandler extends AbstractDialogHandler {
|
||||
|
||||
@@ -79,13 +80,14 @@ export class BrowserDialogHandler extends AbstractDialogHandler {
|
||||
|
||||
async about(): Promise<void> {
|
||||
const detailString = (useAgo: boolean): string => {
|
||||
const releaseString = getReleaseString();
|
||||
return localize('aboutDetail',
|
||||
"Version: {0}\nCommit: {1}\nDate: {2}\nBrowser: {3}",
|
||||
this.productService.version || 'Unknown',
|
||||
this.productService.commit || 'Unknown',
|
||||
this.productService.date ? `${this.productService.date}${useAgo ? ' (' + fromNow(new Date(this.productService.date), true) + ')' : ''}` : 'Unknown',
|
||||
navigator.userAgent
|
||||
);
|
||||
).replace('\n', `\n${releaseString} ${this.productService.release || 'Unknown'}\n`);
|
||||
};
|
||||
|
||||
const detail = detailString(true);
|
||||
|
||||
@@ -22,10 +22,12 @@ import { ColorScheme } from '../../web.api.js';
|
||||
import { OpenFileFolderAction, OpenFolderAction } from '../../actions/workspaceActions.js';
|
||||
import { IWindowOpenable } from '../../../../platform/window/common/window.js';
|
||||
import { splitRecentLabel } from '../../../../base/common/labels.js';
|
||||
import { IViewsService } from '../../../services/views/common/viewsService.js';
|
||||
|
||||
/* eslint-disable */ // Void
|
||||
import { VOID_CTRL_K_ACTION_ID, VOID_CTRL_L_ACTION_ID } from '../../../contrib/void/browser/actionIDs.js';
|
||||
import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../contrib/void/browser/voidSettingsPane.js';
|
||||
import { VIEWLET_ID as REMOTE_EXPLORER_VIEWLET_ID } from '../../../contrib/remote/browser/remoteExplorer.js';
|
||||
/* eslint-enable */
|
||||
|
||||
// interface WatermarkEntry {
|
||||
@@ -98,6 +100,7 @@ export class EditorGroupWatermark extends Disposable {
|
||||
@ICommandService private readonly commandService: ICommandService,
|
||||
@IHostService private readonly hostService: IHostService,
|
||||
@ILabelService private readonly labelService: ILabelService,
|
||||
@IViewsService private readonly viewsService: IViewsService,
|
||||
) {
|
||||
super();
|
||||
|
||||
@@ -182,14 +185,21 @@ export class EditorGroupWatermark extends Disposable {
|
||||
// Void - if the workbench is empty, show open
|
||||
if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) {
|
||||
|
||||
// Create a flex container for buttons with vertical direction
|
||||
const buttonContainer = $('div');
|
||||
buttonContainer.style.display = 'flex';
|
||||
buttonContainer.style.flexDirection = 'column'; // Change to column for vertical stacking
|
||||
buttonContainer.style.alignItems = 'center'; // Center the buttons horizontally
|
||||
buttonContainer.style.gap = '8px'; // Reduce gap between buttons from 16px to 8px
|
||||
buttonContainer.style.marginBottom = '16px';
|
||||
voidIconBox.appendChild(buttonContainer);
|
||||
|
||||
// Open a folder
|
||||
const openFolderButton = h('button')
|
||||
openFolderButton.root.classList.add('void-watermark-button')
|
||||
openFolderButton.root.classList.add('void-openfolder-button')
|
||||
openFolderButton.root.style.display = 'block'
|
||||
openFolderButton.root.style.marginLeft = 'auto'
|
||||
openFolderButton.root.style.marginRight = 'auto'
|
||||
openFolderButton.root.style.marginBottom = '16px'
|
||||
openFolderButton.root.textContent = 'Open a folder'
|
||||
openFolderButton.root.style.width = '124px' // Set width to 124px as requested
|
||||
openFolderButton.root.textContent = 'Open Folder'
|
||||
openFolderButton.root.onclick = () => {
|
||||
this.commandService.executeCommand(isMacintosh && isNative ? OpenFileFolderAction.ID : OpenFolderAction.ID)
|
||||
// if (this.contextKeyService.contextMatchesRules(ContextKeyExpr.and(WorkbenchStateContext.isEqualTo('workspace')))) {
|
||||
@@ -198,7 +208,19 @@ export class EditorGroupWatermark extends Disposable {
|
||||
// this.commandService.executeCommand(isMacintosh ? 'workbench.action.files.openFileFolder' : 'workbench.action.files.openFolder');
|
||||
// }
|
||||
}
|
||||
voidIconBox.appendChild(openFolderButton.root);
|
||||
buttonContainer.appendChild(openFolderButton.root);
|
||||
|
||||
// Open SSH button
|
||||
const openSSHButton = h('button')
|
||||
openSSHButton.root.classList.add('void-openssh-button')
|
||||
openSSHButton.root.style.display = 'block'
|
||||
openSSHButton.root.style.backgroundColor = '#5a5a5a' // Made darker than the default gray
|
||||
openSSHButton.root.style.width = '124px' // Set width to 124px as requested
|
||||
openSSHButton.root.textContent = 'Open SSH'
|
||||
openSSHButton.root.onclick = () => {
|
||||
this.viewsService.openViewContainer(REMOTE_EXPLORER_VIEWLET_ID);
|
||||
}
|
||||
buttonContainer.appendChild(openSSHButton.root);
|
||||
|
||||
|
||||
// Recents
|
||||
@@ -244,13 +266,18 @@ export class EditorGroupWatermark extends Disposable {
|
||||
|
||||
const dirSpan = $('span');
|
||||
dirSpan.style.paddingLeft = '4px';
|
||||
dirSpan.style.whiteSpace = 'nowrap';
|
||||
dirSpan.style.overflow = 'hidden';
|
||||
dirSpan.style.maxWidth = '300px';
|
||||
dirSpan.innerText = parentPath;
|
||||
dirSpan.title = fullPath;
|
||||
|
||||
linkSpan.appendChild(dirSpan);
|
||||
|
||||
return linkSpan
|
||||
}).filter(v => !!v)
|
||||
})
|
||||
.filter(v => !!v)
|
||||
.slice(0, 5) // take 5 most recent
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
}
|
||||
|
||||
/* light */
|
||||
.void-void-icon,
|
||||
.monaco-workbench .part.editor > .content .editor-group-container > .editor-group-watermark > .letterpress {
|
||||
width: 100%;
|
||||
max-height: 100%;
|
||||
@@ -60,14 +61,17 @@
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.void-void-icon,
|
||||
.monaco-workbench.vs-dark .part.editor > .content .editor-group-container .editor-group-watermark > .letterpress {
|
||||
background-image: url('./void_cube_noshadow.png'); /* // Void */
|
||||
}
|
||||
|
||||
.void-void-icon,
|
||||
.monaco-workbench.hc-light .part.editor > .content .editor-group-container .editor-group-watermark > .letterpress {
|
||||
background-image: url('./void_cube_noshadow.png'); /* // Void */
|
||||
}
|
||||
|
||||
.void-void-icon,
|
||||
.monaco-workbench.hc-black .part.editor > .content .editor-group-container .editor-group-watermark > .letterpress {
|
||||
background-image: url('./void_cube_noshadow.png'); /* // Void */
|
||||
}
|
||||
|
||||
@@ -612,7 +612,7 @@ const registry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Con
|
||||
'description': localize('workbench.hover.delay', "Controls the delay in milliseconds after which the hover is shown for workbench items (ex. some extension provided tree view items). Already visible items may require a refresh before reflecting this setting change."),
|
||||
// Testing has indicated that on Windows and Linux 500 ms matches the native hovers most closely.
|
||||
// On Mac, the delay is 1500.
|
||||
'default': isMacintosh ? 1500 : 500,
|
||||
'default': 300, // Void changed this from isMacintosh ? 1500 : 500,
|
||||
'minimum': 0
|
||||
},
|
||||
'workbench.reduceMotion': {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
// added by VSCodium
|
||||
import { language } from '../../../vs/base/common/platform.js';
|
||||
|
||||
const DEFAULT_LABEL = 'Release:';
|
||||
const LABELS: { [key: string]: string } = {
|
||||
'en': DEFAULT_LABEL,
|
||||
'fr': 'Révision :',
|
||||
'ru': 'Релиз:',
|
||||
'zh-hans': '发布版本:',
|
||||
'zh-hant': '發布版本:',
|
||||
};
|
||||
|
||||
export function getReleaseString(): string {
|
||||
return LABELS[language] ?? DEFAULT_LABEL;
|
||||
}
|
||||
@@ -648,15 +648,18 @@ const defaultChat = {
|
||||
providerSetting: product.defaultChatAgent?.providerSetting ?? '',
|
||||
};
|
||||
|
||||
// Void commented this out - copilot head
|
||||
// Add next to the command center if command center is disabled
|
||||
MenuRegistry.appendMenuItem(MenuId.CommandCenter, {
|
||||
/* MenuRegistry.appendMenuItem(MenuId.CommandCenter, {
|
||||
submenu: MenuId.ChatTitleBarMenu,
|
||||
title: localize('title4', "Copilot"),
|
||||
icon: Codicon.copilot,
|
||||
when: ContextKeyExpr.and(
|
||||
ChatContextKeys.supported,
|
||||
ContextKeyExpr.has('config.chat.commandCenter.enabled')
|
||||
),
|
||||
// Void commented this out - copilot head
|
||||
when: ContextKeyExpr.false(),
|
||||
// when: ContextKeyExpr.and(
|
||||
// ChatContextKeys.supported,
|
||||
// ContextKeyExpr.has('config.chat.commandCenter.enabled')
|
||||
// ),
|
||||
order: 10001 // to the right of command center
|
||||
});
|
||||
|
||||
@@ -666,13 +669,15 @@ MenuRegistry.appendMenuItem(MenuId.TitleBar, {
|
||||
title: localize('title4', "Copilot"),
|
||||
group: 'navigation',
|
||||
icon: Codicon.copilot,
|
||||
when: ContextKeyExpr.and(
|
||||
ChatContextKeys.supported,
|
||||
ContextKeyExpr.has('config.chat.commandCenter.enabled'),
|
||||
ContextKeyExpr.has('config.window.commandCenter').negate(),
|
||||
),
|
||||
when: ContextKeyExpr.false(),
|
||||
// Void commented this out - copilot head
|
||||
// when: ContextKeyExpr.and(
|
||||
// ChatContextKeys.supported,
|
||||
// ContextKeyExpr.has('config.chat.commandCenter.enabled'),
|
||||
// ContextKeyExpr.has('config.window.commandCenter').negate(),
|
||||
// ),
|
||||
order: 1
|
||||
});
|
||||
}); */
|
||||
|
||||
registerAction2(class ToggleCopilotControl extends ToggleTitleBarConfigAction {
|
||||
constructor() {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js';
|
||||
import { localize } from '../../../../nls.js';
|
||||
import { Action2, MenuId, MenuRegistry, registerAction2 } from '../../../../platform/actions/common/actions.js';
|
||||
import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js';
|
||||
import { ContextKeyExpr, IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
|
||||
import { IEnvironmentService } from '../../../../platform/environment/common/environment.js';
|
||||
import { IFileService } from '../../../../platform/files/common/files.js';
|
||||
@@ -15,7 +15,7 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../platfo
|
||||
import { createSyncHeaders, IAuthenticationProvider, IResourceRefHandle } from '../../../../platform/userDataSync/common/userDataSync.js';
|
||||
import { AuthenticationSession, AuthenticationSessionsChangeEvent, IAuthenticationService } from '../../../services/authentication/common/authentication.js';
|
||||
import { IExtensionService } from '../../../services/extensions/common/extensions.js';
|
||||
import { EDIT_SESSIONS_SIGNED_IN, EditSession, EDIT_SESSION_SYNC_CATEGORY, IEditSessionsStorageService, EDIT_SESSIONS_SIGNED_IN_KEY, IEditSessionsLogService, SyncResource, EDIT_SESSIONS_PENDING_KEY } from '../common/editSessions.js';
|
||||
import { EDIT_SESSIONS_SIGNED_IN, EditSession, EDIT_SESSION_SYNC_CATEGORY, IEditSessionsStorageService, EDIT_SESSIONS_SIGNED_IN_KEY, IEditSessionsLogService, SyncResource } from '../common/editSessions.js';
|
||||
import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js';
|
||||
import { generateUuid } from '../../../../base/common/uuid.js';
|
||||
import { getCurrentAuthenticationSessionInfo } from '../../../services/authentication/browser/authenticationService.js';
|
||||
@@ -91,7 +91,6 @@ export class EditSessionsWorkbenchService extends Disposable implements IEditSes
|
||||
// If another window changes the preferred session storage, reset our cached auth state in memory
|
||||
this._register(this.storageService.onDidChangeValue(StorageScope.APPLICATION, EditSessionsWorkbenchService.CACHED_SESSION_STORAGE_KEY, this._store)(() => this.onDidChangeStorage()));
|
||||
|
||||
this.registerSignInAction();
|
||||
this.registerResetAuthenticationAction();
|
||||
|
||||
this.signedInContext = EDIT_SESSIONS_SIGNED_IN.bindTo(this.contextKeyService);
|
||||
@@ -454,46 +453,6 @@ export class EditSessionsWorkbenchService extends Disposable implements IEditSes
|
||||
}
|
||||
}
|
||||
|
||||
private registerSignInAction() {
|
||||
if (!this.serverConfiguration?.url) {
|
||||
return;
|
||||
}
|
||||
const that = this;
|
||||
const id = 'workbench.editSessions.actions.signIn';
|
||||
const when = ContextKeyExpr.and(ContextKeyExpr.equals(EDIT_SESSIONS_PENDING_KEY, false), ContextKeyExpr.equals(EDIT_SESSIONS_SIGNED_IN_KEY, false));
|
||||
this._register(registerAction2(class ResetEditSessionAuthenticationAction extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id,
|
||||
title: localize('sign in', 'Turn on Cloud Changes...'),
|
||||
category: EDIT_SESSION_SYNC_CATEGORY,
|
||||
precondition: when,
|
||||
menu: [{
|
||||
id: MenuId.CommandPalette,
|
||||
},
|
||||
{
|
||||
id: MenuId.AccountsContext,
|
||||
group: '2_editSessions',
|
||||
when,
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
async run() {
|
||||
return await that.initialize('write', false);
|
||||
}
|
||||
}));
|
||||
|
||||
this._register(MenuRegistry.appendMenuItem(MenuId.AccountsContext, {
|
||||
group: '2_editSessions',
|
||||
command: {
|
||||
id,
|
||||
title: localize('sign in badge', 'Turn on Cloud Changes... (1)'),
|
||||
},
|
||||
when: ContextKeyExpr.and(ContextKeyExpr.equals(EDIT_SESSIONS_PENDING_KEY, true), ContextKeyExpr.equals(EDIT_SESSIONS_SIGNED_IN_KEY, false))
|
||||
}));
|
||||
}
|
||||
|
||||
private registerResetAuthenticationAction() {
|
||||
const that = this;
|
||||
this._register(registerAction2(class ResetEditSessionAuthenticationAction extends Action2 {
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js';
|
||||
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
|
||||
import { localize2 } from '../../../../nls.js';
|
||||
import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js';
|
||||
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';
|
||||
import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js';
|
||||
|
||||
|
||||
// to change this, just Cmd+Shift+F and replace DummyService with YourServiceName, and create a unique ID below
|
||||
export interface IDummyService {
|
||||
readonly _serviceBrand: undefined; // services need this, just leave it undefined
|
||||
}
|
||||
|
||||
export const IDummyService = createDecorator<IDummyService>('DummyService');
|
||||
|
||||
|
||||
|
||||
// An example of an action (delete if you're not using an action):
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
f1: true,
|
||||
id: 'void.dummy',
|
||||
title: localize2('dummy', 'dummy: Init'),
|
||||
keybinding: {
|
||||
primary: KeyMod.CtrlCmd | KeyCode.Digit0,
|
||||
weight: KeybindingWeight.VoidExtension,
|
||||
}
|
||||
});
|
||||
}
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const n = accessor.get(IDummyService)
|
||||
console.log('Hi', n._serviceBrand)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
class DummyService extends Disposable implements IWorkbenchContribution, IDummyService {
|
||||
static readonly ID = 'workbench.contrib.void.dummy' // workbenchContributions need this, services do not
|
||||
_serviceBrand: undefined;
|
||||
|
||||
constructor(
|
||||
@ICodeEditorService codeEditorService: ICodeEditorService,
|
||||
) {
|
||||
super()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// pick one and delete the other:
|
||||
registerSingleton(IDummyService, DummyService, InstantiationType.Eager); // lazily loaded, even if Eager
|
||||
|
||||
registerWorkbenchContribution2(DummyService.ID, DummyService, WorkbenchPhase.BlockRestore); // mounts on start
|
||||
@@ -34,7 +34,7 @@
|
||||
// // const result = await new Promise((res, rej) => {
|
||||
// // sendLLMMessage({
|
||||
// // messages,
|
||||
// // tools: ['text_search'],
|
||||
// // tools: ['search_for_files'],
|
||||
// // onFinalMessage: ({ result: r, }) => {
|
||||
// // res(r)
|
||||
// // },
|
||||
@@ -73,7 +73,7 @@
|
||||
// // const result = new Promise((res, rej) => {
|
||||
// // sendLLMMessage({
|
||||
// // messages,
|
||||
// // tools: ['text_search'],
|
||||
// // tools: ['search_for_files'],
|
||||
// // onResult: (r) => {
|
||||
// // res(r)
|
||||
// // }
|
||||
|
||||
@@ -8,8 +8,7 @@ import { ILanguageFeaturesService } from '../../../../editor/common/services/lan
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { ITextModel } from '../../../../editor/common/model.js';
|
||||
import { Position } from '../../../../editor/common/core/position.js';
|
||||
import { InlineCompletion, InlineCompletionContext, } from '../../../../editor/common/languages.js';
|
||||
import { CancellationToken } from '../../../../base/common/cancellation.js';
|
||||
import { InlineCompletion, } from '../../../../editor/common/languages.js';
|
||||
import { Range } from '../../../../editor/common/core/range.js';
|
||||
import { IEditorService } from '../../../services/editor/common/editorService.js';
|
||||
import { isCodeEditor } from '../../../../editor/browser/editorBrowser.js';
|
||||
@@ -21,6 +20,7 @@ import { ILLMMessageService } from '../common/sendLLMMessageService.js';
|
||||
import { isWindows } from '../../../../base/common/platform.js';
|
||||
import { IVoidSettingsService } from '../common/voidSettingsService.js';
|
||||
import { FeatureName } from '../common/voidSettingsTypes.js';
|
||||
import { IConvertToLLMMessageService } from './convertToLLMMessageService.js';
|
||||
// import { IContextGatheringService } from './contextGatheringService.js';
|
||||
|
||||
|
||||
@@ -633,8 +633,6 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
|
||||
async _provideInlineCompletionItems(
|
||||
model: ITextModel,
|
||||
position: Position,
|
||||
context: InlineCompletionContext,
|
||||
token: CancellationToken,
|
||||
): Promise<InlineCompletion[]> {
|
||||
|
||||
const isEnabled = this._settingsService.state.globalSettings.enableAutocomplete
|
||||
@@ -793,19 +791,20 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
|
||||
|
||||
const featureName: FeatureName = 'Autocomplete'
|
||||
const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName]
|
||||
const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[modelSelection.providerName]?.[modelSelection.modelName] : undefined
|
||||
|
||||
const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName] : undefined
|
||||
|
||||
// set parameters of `newAutocompletion` appropriately
|
||||
newAutocompletion.llmPromise = new Promise((resolve, reject) => {
|
||||
|
||||
const requestId = this._llmMessageService.sendLLMMessage({
|
||||
messagesType: 'FIMMessage',
|
||||
messages: {
|
||||
prefix: llmPrefix,
|
||||
suffix: llmSuffix,
|
||||
stopTokens: stopTokens,
|
||||
},
|
||||
messages: this._convertToLLMMessageService.prepareFIMMessage({
|
||||
messages: {
|
||||
prefix: llmPrefix,
|
||||
suffix: llmSuffix,
|
||||
stopTokens: stopTokens,
|
||||
}
|
||||
}),
|
||||
modelSelection,
|
||||
modelSelectionOptions,
|
||||
logging: { loggingName: 'Autocomplete' },
|
||||
@@ -852,7 +851,7 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
|
||||
newAutocompletion.status = 'error'
|
||||
reject(message)
|
||||
},
|
||||
onAbort: () => { },
|
||||
onAbort: () => { reject('Aborted autocomplete') },
|
||||
})
|
||||
newAutocompletion.requestId = requestId
|
||||
|
||||
@@ -893,13 +892,14 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
|
||||
@IEditorService private readonly _editorService: IEditorService,
|
||||
@IModelService private readonly _modelService: IModelService,
|
||||
@IVoidSettingsService private readonly _settingsService: IVoidSettingsService,
|
||||
@IConvertToLLMMessageService private readonly _convertToLLMMessageService: IConvertToLLMMessageService
|
||||
// @IContextGatheringService private readonly _contextGatheringService: IContextGatheringService,
|
||||
) {
|
||||
super()
|
||||
|
||||
this._langFeatureService.inlineCompletionsProvider.register('*', {
|
||||
this._register(this._langFeatureService.inlineCompletionsProvider.register('*', {
|
||||
provideInlineCompletions: async (model, position, context, token) => {
|
||||
const items = await this._provideInlineCompletionItems(model, position, context, token)
|
||||
const items = await this._provideInlineCompletionItems(model, position)
|
||||
|
||||
// console.log('item: ', items?.[0]?.insertText)
|
||||
return { items: items, }
|
||||
@@ -936,7 +936,7 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
|
||||
});
|
||||
|
||||
},
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,648 @@
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { deepClone } from '../../../../base/common/objects.js';
|
||||
import { IModelService } from '../../../../editor/common/services/model.js';
|
||||
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
|
||||
import { IEditorService } from '../../../services/editor/common/editorService.js';
|
||||
import { ChatMessage } from '../common/chatThreadServiceTypes.js';
|
||||
import { getIsReasoningEnabledState, getMaxOutputTokens, getModelCapabilities } from '../common/modelCapabilities.js';
|
||||
import { reParsedToolXMLString, chat_systemMessage, ToolName } from '../common/prompt/prompts.js';
|
||||
import { AnthropicLLMChatMessage, AnthropicReasoning, LLMChatMessage, LLMFIMMessage, OpenAILLMChatMessage, RawToolParamsObj } from '../common/sendLLMMessageTypes.js';
|
||||
import { IVoidSettingsService } from '../common/voidSettingsService.js';
|
||||
import { ChatMode, FeatureName, ModelSelection } from '../common/voidSettingsTypes.js';
|
||||
import { IDirectoryStrService } from './directoryStrService.js';
|
||||
import { ITerminalToolService } from './terminalToolService.js';
|
||||
import { IVoidModelService } from '../common/voidModelService.js';
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
|
||||
|
||||
|
||||
|
||||
type SimpleLLMMessage = {
|
||||
role: 'tool';
|
||||
content: string;
|
||||
id: string;
|
||||
name: ToolName;
|
||||
rawParams: RawToolParamsObj;
|
||||
} | {
|
||||
role: 'user';
|
||||
content: string;
|
||||
} | {
|
||||
role: 'assistant';
|
||||
content: string;
|
||||
anthropicReasoning: AnthropicReasoning[] | null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
const EMPTY_MESSAGE = '(empty message)'
|
||||
|
||||
const CHARS_PER_TOKEN = 4
|
||||
const TRIM_TO_LEN = 60
|
||||
|
||||
|
||||
|
||||
|
||||
// convert messages as if about to send to openai
|
||||
/*
|
||||
reference - https://platform.openai.com/docs/guides/function-calling#function-calling-steps
|
||||
openai MESSAGE (role=assistant):
|
||||
"tool_calls":[{
|
||||
"type": "function",
|
||||
"id": "call_12345xyz",
|
||||
"function": {
|
||||
"name": "get_weather",
|
||||
"arguments": "{\"latitude\":48.8566,\"longitude\":2.3522}"
|
||||
}]
|
||||
|
||||
openai RESPONSE (role=user):
|
||||
{ "role": "tool",
|
||||
"tool_call_id": tool_call.id,
|
||||
"content": str(result) }
|
||||
|
||||
also see
|
||||
openai on prompting - https://platform.openai.com/docs/guides/reasoning#advice-on-prompting
|
||||
openai on developer system message - https://cdn.openai.com/spec/model-spec-2024-05-08.html#follow-the-chain-of-command
|
||||
*/
|
||||
|
||||
|
||||
const prepareMessages_openai_tools = (messages: SimpleLLMMessage[]): LLMChatMessage[] => {
|
||||
|
||||
const newMessages: OpenAILLMChatMessage[] = [];
|
||||
|
||||
for (let i = 0; i < messages.length; i += 1) {
|
||||
const currMsg = messages[i]
|
||||
|
||||
if (currMsg.role !== 'tool') {
|
||||
newMessages.push(currMsg)
|
||||
continue
|
||||
}
|
||||
|
||||
// edit previous assistant message to have called the tool
|
||||
const prevMsg = 0 <= i - 1 && i - 1 <= newMessages.length ? newMessages[i - 1] : undefined
|
||||
if (prevMsg?.role === 'assistant') {
|
||||
prevMsg.tool_calls = [{
|
||||
type: 'function',
|
||||
id: currMsg.id,
|
||||
function: {
|
||||
name: currMsg.name,
|
||||
arguments: JSON.stringify(currMsg.rawParams)
|
||||
}
|
||||
}]
|
||||
}
|
||||
|
||||
// add the tool
|
||||
newMessages.push({
|
||||
role: 'tool',
|
||||
tool_call_id: currMsg.id,
|
||||
content: currMsg.content,
|
||||
})
|
||||
}
|
||||
return newMessages
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// convert messages as if about to send to anthropic
|
||||
/*
|
||||
https://docs.anthropic.com/en/docs/build-with-claude/tool-use#tool-use-examples
|
||||
anthropic MESSAGE (role=assistant):
|
||||
"content": [{
|
||||
"type": "text",
|
||||
"text": "<thinking>I need to call the get_weather function, and the user wants SF, which is likely San Francisco, CA.</thinking>"
|
||||
}, {
|
||||
"type": "tool_use",
|
||||
"id": "toolu_01A09q90qw90lq917835lq9",
|
||||
"name": "get_weather",
|
||||
"input": { "location": "San Francisco, CA", "unit": "celsius" }
|
||||
}]
|
||||
anthropic RESPONSE (role=user):
|
||||
"content": [{
|
||||
"type": "tool_result",
|
||||
"tool_use_id": "toolu_01A09q90qw90lq917835lq9",
|
||||
"content": "15 degrees"
|
||||
}]
|
||||
|
||||
|
||||
Converts:
|
||||
assistant: ...content
|
||||
tool: (id, name, params)
|
||||
->
|
||||
assistant: ...content, call(name, id, params)
|
||||
user: ...content, result(id, content)
|
||||
*/
|
||||
|
||||
|
||||
const prepareMessages_anthropic_tools = (messages: SimpleLLMMessage[], supportsAnthropicReasoning: boolean): LLMChatMessage[] => {
|
||||
const newMessages: (AnthropicLLMChatMessage | (SimpleLLMMessage & { role: 'tool' }))[] = messages;
|
||||
|
||||
for (let i = 0; i < messages.length; i += 1) {
|
||||
const currMsg = messages[i]
|
||||
|
||||
// add anthropic reasoning
|
||||
if (currMsg.role === 'assistant') {
|
||||
if (currMsg.anthropicReasoning && supportsAnthropicReasoning) {
|
||||
const content = currMsg.content
|
||||
newMessages[i] = {
|
||||
role: 'assistant',
|
||||
content: content ? [...currMsg.anthropicReasoning, { type: 'text' as const, text: content }] : currMsg.anthropicReasoning
|
||||
}
|
||||
}
|
||||
else {
|
||||
newMessages[i] = {
|
||||
role: 'assistant',
|
||||
content: currMsg.content,
|
||||
// strip away anthropicReasoning
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (currMsg.role === 'user') {
|
||||
newMessages[i] = {
|
||||
role: 'user',
|
||||
content: currMsg.content,
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (currMsg.role === 'tool') {
|
||||
// add anthropic tools
|
||||
const prevMsg = 0 <= i - 1 && i - 1 <= newMessages.length ? newMessages[i - 1] : undefined
|
||||
|
||||
// make it so the assistant called the tool
|
||||
if (prevMsg?.role === 'assistant') {
|
||||
if (typeof prevMsg.content === 'string') prevMsg.content = [{ type: 'text', text: prevMsg.content }]
|
||||
prevMsg.content.push({ type: 'tool_use', id: currMsg.id, name: currMsg.name, input: currMsg.rawParams })
|
||||
}
|
||||
|
||||
// turn each tool into a user message with tool results at the end
|
||||
newMessages[i] = {
|
||||
role: 'user',
|
||||
content: [{ type: 'tool_result', tool_use_id: currMsg.id, content: currMsg.content }]
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// we just removed the tools
|
||||
return newMessages as AnthropicLLMChatMessage[]
|
||||
}
|
||||
|
||||
|
||||
const prepareMessages_XML_tools = (messages: SimpleLLMMessage[], supportsAnthropicReasoning: boolean): LLMChatMessage[] => {
|
||||
|
||||
const llmChatMessages: LLMChatMessage[] = [];
|
||||
for (let i = 0; i < messages.length; i += 1) {
|
||||
|
||||
const c = messages[i]
|
||||
const next = 0 <= i + 1 && i + 1 <= messages.length - 1 ? messages[i + 1] : null
|
||||
|
||||
if (c.role === 'assistant') {
|
||||
// if called a tool (message after it), re-add its XML to the message
|
||||
// alternatively, could just hold onto the original output, but this way requires less piping raw strings everywhere
|
||||
let content: LLMChatMessage['content'] = c.content
|
||||
if (next?.role === 'tool') {
|
||||
content = `${content}\n\n${reParsedToolXMLString(next.name, next.rawParams)}`
|
||||
}
|
||||
|
||||
// anthropic reasoning
|
||||
if (c.anthropicReasoning && supportsAnthropicReasoning) {
|
||||
content = content ? [...c.anthropicReasoning, { type: 'text' as const, text: content }] : c.anthropicReasoning
|
||||
}
|
||||
llmChatMessages.push({
|
||||
role: 'assistant',
|
||||
content
|
||||
})
|
||||
}
|
||||
// add user or tool to the previous user message
|
||||
else if (c.role === 'user' || c.role === 'tool') {
|
||||
if (c.role === 'tool')
|
||||
c.content = `<${c.name}_result>\n${c.content}\n</${c.name}_result>`
|
||||
|
||||
if (llmChatMessages.length === 0 || llmChatMessages[llmChatMessages.length - 1].role !== 'user')
|
||||
llmChatMessages.push({
|
||||
role: 'user',
|
||||
content: c.content
|
||||
})
|
||||
else
|
||||
llmChatMessages[llmChatMessages.length - 1].content += '\n\n' + c.content
|
||||
}
|
||||
}
|
||||
return llmChatMessages
|
||||
}
|
||||
|
||||
|
||||
|
||||
const prepareMessages_providerSpecific = (messages: SimpleLLMMessage[], specialToolFormat: 'openai-style' | 'anthropic-style' | undefined, supportsAnthropicReasoning: boolean): LLMChatMessage[] => {
|
||||
const llmChatMessages: LLMChatMessage[] = []
|
||||
if (!specialToolFormat) { // XML tool behavior
|
||||
return prepareMessages_XML_tools(messages, supportsAnthropicReasoning)
|
||||
}
|
||||
else if (specialToolFormat === 'anthropic-style') {
|
||||
return prepareMessages_anthropic_tools(messages, supportsAnthropicReasoning)
|
||||
}
|
||||
else if (specialToolFormat === 'openai-style') {
|
||||
return prepareMessages_openai_tools(messages)
|
||||
}
|
||||
return llmChatMessages
|
||||
}
|
||||
|
||||
|
||||
// --- CHAT ---
|
||||
|
||||
const prepareMessages = ({
|
||||
messages,
|
||||
systemMessage,
|
||||
aiInstructions,
|
||||
supportsSystemMessage,
|
||||
specialToolFormat,
|
||||
supportsAnthropicReasoning,
|
||||
contextWindow,
|
||||
maxOutputTokens,
|
||||
}: {
|
||||
messages: SimpleLLMMessage[],
|
||||
systemMessage: string,
|
||||
aiInstructions: string,
|
||||
supportsSystemMessage: false | 'system-role' | 'developer-role' | 'separated',
|
||||
specialToolFormat: 'openai-style' | 'anthropic-style' | undefined,
|
||||
supportsAnthropicReasoning: boolean,
|
||||
contextWindow: number,
|
||||
maxOutputTokens: number | null | undefined,
|
||||
}): { messages: LLMChatMessage[], separateSystemMessage: string | undefined } => {
|
||||
maxOutputTokens = maxOutputTokens ?? 4_096 // default to 4096
|
||||
|
||||
// ================ trim ================
|
||||
|
||||
messages = deepClone(messages)
|
||||
messages = messages.map(m => ({ ...m, content: m.role !== 'tool' ? m.content.trim() : m.content }))
|
||||
|
||||
// ================ fit into context ================
|
||||
|
||||
// the higher the weight, the higher the desire to truncate - TRIM HIGHEST WEIGHT MESSAGES
|
||||
const alreadyTrimmedIdxes = new Set<number>()
|
||||
const weight = (message: SimpleLLMMessage, messages: SimpleLLMMessage[], idx: number) => {
|
||||
const base = message.content.length
|
||||
|
||||
let multiplier: number
|
||||
multiplier = 1 + (messages.length - 1 - idx) / messages.length // slow rampdown from 2 to 1 as index increases
|
||||
if (message.role === 'user') {
|
||||
multiplier *= 1
|
||||
}
|
||||
else {
|
||||
multiplier *= 10 // llm tokens are far less valuable than user tokens
|
||||
}
|
||||
// 1st message, last 3 msgs, any already modified message should be low in weight
|
||||
if (idx === 0 || idx >= messages.length - 1 - 3 || alreadyTrimmedIdxes.has(idx)) {
|
||||
multiplier *= .05
|
||||
}
|
||||
return base * multiplier
|
||||
}
|
||||
|
||||
const _findLargestByWeight = (messages: SimpleLLMMessage[]) => {
|
||||
let largestIndex = -1
|
||||
let largestWeight = -Infinity
|
||||
for (let i = 0; i < messages.length; i += 1) {
|
||||
const m = messages[i]
|
||||
const w = weight(m, messages, i)
|
||||
if (w > largestWeight) {
|
||||
largestWeight = w
|
||||
largestIndex = i
|
||||
}
|
||||
}
|
||||
return largestIndex
|
||||
}
|
||||
|
||||
let totalLen = 0
|
||||
for (const m of messages) { totalLen += m.content.length }
|
||||
const charsNeedToTrim = totalLen - (contextWindow - maxOutputTokens) * CHARS_PER_TOKEN
|
||||
|
||||
// <----------------------------------------->
|
||||
// 0 | | |
|
||||
// | contextWindow |
|
||||
// contextWindow - maxOut|putTokens
|
||||
// totalLen
|
||||
let remainingCharsToTrim = charsNeedToTrim
|
||||
let i = 0
|
||||
|
||||
while (remainingCharsToTrim > 0) {
|
||||
i += 1
|
||||
if (i > 100) break
|
||||
|
||||
const trimIdx = _findLargestByWeight(messages)
|
||||
const m = messages[trimIdx]
|
||||
|
||||
// if can finish here, do
|
||||
const numCharsWillTrim = m.content.length - TRIM_TO_LEN
|
||||
if (numCharsWillTrim > remainingCharsToTrim) {
|
||||
m.content = m.content.slice(0, m.content.length - remainingCharsToTrim).trim()
|
||||
break
|
||||
}
|
||||
|
||||
remainingCharsToTrim -= numCharsWillTrim
|
||||
m.content = m.content.substring(0, TRIM_TO_LEN - 3) + '...'
|
||||
alreadyTrimmedIdxes.add(trimIdx)
|
||||
}
|
||||
|
||||
// ================ tools and anthropicReasoning ================
|
||||
const llmMessages: LLMChatMessage[] = prepareMessages_providerSpecific(messages, specialToolFormat, supportsAnthropicReasoning)
|
||||
|
||||
// ================ system message concat ================
|
||||
|
||||
// find system messages and concatenate them
|
||||
const newSystemMessage = aiInstructions ?
|
||||
`${(systemMessage ? `${systemMessage}\n\n` : '')}GUIDELINES\n${aiInstructions}`
|
||||
: systemMessage
|
||||
|
||||
let separateSystemMessageStr: string | undefined = undefined
|
||||
|
||||
// if supports system message
|
||||
if (supportsSystemMessage) {
|
||||
if (supportsSystemMessage === 'separated')
|
||||
separateSystemMessageStr = newSystemMessage
|
||||
else if (supportsSystemMessage === 'system-role')
|
||||
llmMessages.unshift({ role: 'system', content: newSystemMessage }) // add new first message
|
||||
else if (supportsSystemMessage === 'developer-role')
|
||||
llmMessages.unshift({ role: 'developer', content: newSystemMessage }) // add new first message
|
||||
}
|
||||
// if does not support system message
|
||||
else {
|
||||
const newFirstMessage = {
|
||||
role: 'user',
|
||||
content: `<SYSTEM_MESSAGE>\n${newSystemMessage}\n</SYSTEM_MESSAGE>\n${llmMessages[0].content}`
|
||||
} as const
|
||||
llmMessages.splice(0, 1) // delete first message
|
||||
llmMessages.unshift(newFirstMessage) // add new first message
|
||||
}
|
||||
|
||||
|
||||
// ================ no empty message ================
|
||||
for (const currMsg of llmMessages) {
|
||||
if (currMsg.role === 'tool') continue
|
||||
|
||||
// if content is a string, replace string with empty msg
|
||||
if (typeof currMsg.content === 'string')
|
||||
currMsg.content = currMsg.content || EMPTY_MESSAGE
|
||||
else {
|
||||
// if content is an array, replace any empty text entries with empty msg, and make sure there's at least 1 entry
|
||||
for (const c of currMsg.content) {
|
||||
if (c.type === 'text') c.text = c.text || EMPTY_MESSAGE
|
||||
}
|
||||
if (currMsg.content.length === 0) currMsg.content = [{ type: 'text', text: EMPTY_MESSAGE }]
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
messages: llmMessages,
|
||||
separateSystemMessage: separateSystemMessageStr,
|
||||
} as const
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export interface IConvertToLLMMessageService {
|
||||
readonly _serviceBrand: undefined;
|
||||
prepareLLMSimpleMessages: (opts: { simpleMessages: SimpleLLMMessage[], systemMessage: string, modelSelection: ModelSelection | null, featureName: FeatureName }) => { messages: LLMChatMessage[], separateSystemMessage: string | undefined }
|
||||
prepareLLMChatMessages: (opts: { chatMessages: ChatMessage[], chatMode: ChatMode, modelSelection: ModelSelection | null }) => Promise<{ messages: LLMChatMessage[], separateSystemMessage: string | undefined }>
|
||||
prepareFIMMessage(opts: { messages: LLMFIMMessage, }): { prefix: string, suffix: string, stopTokens: string[] }
|
||||
|
||||
}
|
||||
|
||||
export const IConvertToLLMMessageService = createDecorator<IConvertToLLMMessageService>('ConvertToLLMMessageService');
|
||||
|
||||
|
||||
class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMessageService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
constructor(
|
||||
@IModelService private readonly modelService: IModelService,
|
||||
@IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService,
|
||||
@IEditorService private readonly editorService: IEditorService,
|
||||
@IDirectoryStrService private readonly directoryStrService: IDirectoryStrService,
|
||||
@ITerminalToolService private readonly terminalToolService: ITerminalToolService,
|
||||
@IVoidSettingsService private readonly voidSettingsService: IVoidSettingsService,
|
||||
@IVoidModelService private readonly voidModelService: IVoidModelService,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
// Read .voidrules files from workspace folders
|
||||
private _getVoidRulesFileContents(): string {
|
||||
try {
|
||||
const workspaceFolders = this.workspaceContextService.getWorkspace().folders;
|
||||
let voidRules = '';
|
||||
for (const folder of workspaceFolders) {
|
||||
const uri = URI.joinPath(folder.uri, '.voidrules')
|
||||
const { model } = this.voidModelService.getModel(uri)
|
||||
if (!model) continue
|
||||
voidRules += model.getValue() + '\n\n';
|
||||
}
|
||||
return voidRules.trim();
|
||||
}
|
||||
catch (e) {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
// Get combined AI instructions from settings and .voidrules files
|
||||
private _getCombinedAIInstructions(): string {
|
||||
const globalAIInstructions = this.voidSettingsService.state.globalSettings.aiInstructions;
|
||||
const voidRulesFileContent = this._getVoidRulesFileContents();
|
||||
|
||||
const ans: string[] = []
|
||||
if (globalAIInstructions) ans.push(globalAIInstructions)
|
||||
if (voidRulesFileContent) ans.push(voidRulesFileContent)
|
||||
return ans.join('\n\n')
|
||||
}
|
||||
|
||||
|
||||
// system message
|
||||
private _generateChatMessagesSystemMessage = async (chatMode: ChatMode, specialToolFormat: 'openai-style' | 'anthropic-style' | undefined) => {
|
||||
const workspaceFolders = this.workspaceContextService.getWorkspace().folders.map(f => f.uri.fsPath)
|
||||
|
||||
const openedURIs = this.modelService.getModels().filter(m => m.isAttachedToEditor()).map(m => m.uri.fsPath) || [];
|
||||
const activeURI = this.editorService.activeEditor?.resource?.fsPath;
|
||||
|
||||
const directoryStr = await this.directoryStrService.getAllDirectoriesStr({
|
||||
cutOffMessage: chatMode === 'agent' || chatMode === 'gather' ?
|
||||
`...Directories string cut off, use tools to read more...`
|
||||
: `...Directories string cut off, ask user for more if necessary...`
|
||||
})
|
||||
const includeXMLToolDefinitions = !specialToolFormat
|
||||
|
||||
const runningTerminalIds = this.terminalToolService.listTerminalIds()
|
||||
const systemMessage = chat_systemMessage({ workspaceFolders, openedURIs, directoryStr, activeURI, runningTerminalIds, chatMode, includeXMLToolDefinitions })
|
||||
return systemMessage
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// --- LLM Chat messages ---
|
||||
|
||||
private _chatMessagesToSimpleMessages(chatMessages: ChatMessage[]): SimpleLLMMessage[] {
|
||||
const simpleLLMMessages: SimpleLLMMessage[] = []
|
||||
|
||||
for (const m of chatMessages) {
|
||||
if (m.role === 'checkpoint') continue
|
||||
if (m.role === 'interrupted_streaming_tool') continue
|
||||
if (m.role === 'assistant') {
|
||||
simpleLLMMessages.push({
|
||||
role: m.role,
|
||||
content: m.displayContent,
|
||||
anthropicReasoning: m.anthropicReasoning,
|
||||
})
|
||||
}
|
||||
else if (m.role === 'tool') {
|
||||
simpleLLMMessages.push({
|
||||
role: m.role,
|
||||
content: m.content,
|
||||
name: m.name,
|
||||
id: m.id,
|
||||
rawParams: m.rawParams,
|
||||
})
|
||||
}
|
||||
else if (m.role === 'user') {
|
||||
simpleLLMMessages.push({
|
||||
role: m.role,
|
||||
content: m.content,
|
||||
})
|
||||
}
|
||||
}
|
||||
return simpleLLMMessages
|
||||
}
|
||||
|
||||
prepareLLMSimpleMessages: IConvertToLLMMessageService['prepareLLMSimpleMessages'] = ({ simpleMessages, systemMessage, modelSelection, featureName }) => {
|
||||
if (modelSelection === null) return { messages: [], separateSystemMessage: undefined }
|
||||
const { providerName, modelName } = modelSelection
|
||||
const {
|
||||
specialToolFormat,
|
||||
contextWindow,
|
||||
supportsSystemMessage,
|
||||
} = getModelCapabilities(providerName, modelName)
|
||||
|
||||
const modelSelectionOptions = this.voidSettingsService.state.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName]
|
||||
|
||||
// Get combined AI instructions
|
||||
const aiInstructions = this._getCombinedAIInstructions();
|
||||
|
||||
const isReasoningEnabled = getIsReasoningEnabledState(featureName, providerName, modelName, modelSelectionOptions)
|
||||
const maxOutputTokens = getMaxOutputTokens(providerName, modelName, { isReasoningEnabled })
|
||||
|
||||
const { messages, separateSystemMessage } = prepareMessages({
|
||||
messages: simpleMessages,
|
||||
systemMessage,
|
||||
aiInstructions,
|
||||
supportsSystemMessage,
|
||||
specialToolFormat,
|
||||
supportsAnthropicReasoning: providerName === 'anthropic',
|
||||
contextWindow,
|
||||
maxOutputTokens,
|
||||
})
|
||||
return { messages, separateSystemMessage };
|
||||
}
|
||||
prepareLLMChatMessages: IConvertToLLMMessageService['prepareLLMChatMessages'] = async ({ chatMessages, chatMode, modelSelection }) => {
|
||||
if (modelSelection === null) return { messages: [], separateSystemMessage: undefined }
|
||||
const { providerName, modelName } = modelSelection
|
||||
const {
|
||||
specialToolFormat,
|
||||
contextWindow,
|
||||
supportsSystemMessage,
|
||||
} = getModelCapabilities(providerName, modelName)
|
||||
const systemMessage = await this._generateChatMessagesSystemMessage(chatMode, specialToolFormat)
|
||||
|
||||
const modelSelectionOptions = this.voidSettingsService.state.optionsOfModelSelection['Chat'][modelSelection.providerName]?.[modelSelection.modelName]
|
||||
|
||||
// Get combined AI instructions
|
||||
const aiInstructions = this._getCombinedAIInstructions();
|
||||
|
||||
const isReasoningEnabled = getIsReasoningEnabledState('Chat', providerName, modelName, modelSelectionOptions)
|
||||
const maxOutputTokens = getMaxOutputTokens(providerName, modelName, { isReasoningEnabled })
|
||||
const llmMessages = this._chatMessagesToSimpleMessages(chatMessages)
|
||||
|
||||
const { messages, separateSystemMessage } = prepareMessages({
|
||||
messages: llmMessages,
|
||||
systemMessage,
|
||||
aiInstructions,
|
||||
supportsSystemMessage,
|
||||
specialToolFormat,
|
||||
supportsAnthropicReasoning: providerName === 'anthropic',
|
||||
contextWindow,
|
||||
maxOutputTokens,
|
||||
})
|
||||
return { messages, separateSystemMessage };
|
||||
}
|
||||
|
||||
|
||||
// --- FIM ---
|
||||
|
||||
prepareFIMMessage: IConvertToLLMMessageService['prepareFIMMessage'] = ({ messages }) => {
|
||||
// Get combined AI instructions with the provided aiInstructions as the base
|
||||
const combinedInstructions = this._getCombinedAIInstructions();
|
||||
|
||||
let prefix = `\
|
||||
${!combinedInstructions ? '' : `\
|
||||
// Instructions:
|
||||
// Do not output an explanation. Try to avoid outputting comments. Only output the middle code.
|
||||
${combinedInstructions.split('\n').map(line => `//${line}`).join('\n')}`}
|
||||
|
||||
${messages.prefix}`
|
||||
|
||||
const suffix = messages.suffix
|
||||
const stopTokens = messages.stopTokens
|
||||
return { prefix, suffix, stopTokens }
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
registerSingleton(IConvertToLLMMessageService, ConvertToLLMMessageService, InstantiationType.Eager);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/*
|
||||
Gemini has this, but they're openai-compat so we don't need to implement this
|
||||
gemini request:
|
||||
{ "role": "assistant",
|
||||
"content": null,
|
||||
"function_call": {
|
||||
"name": "get_weather",
|
||||
"arguments": {
|
||||
"latitude": 48.8566,
|
||||
"longitude": 2.3522
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
gemini response:
|
||||
{ "role": "assistant",
|
||||
"function_response": {
|
||||
"name": "get_weather",
|
||||
"response": {
|
||||
"temperature": "15°C",
|
||||
"condition": "Cloudy"
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
|
||||
import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js';
|
||||
import { IVoidModelService } from '../common/voidModelService.js';
|
||||
|
||||
class ConvertContribWorkbenchContribution extends Disposable implements IWorkbenchContribution {
|
||||
static readonly ID = 'workbench.contrib.void.convertcontrib'
|
||||
_serviceBrand: undefined;
|
||||
|
||||
constructor(
|
||||
@IVoidModelService private readonly voidModelService: IVoidModelService,
|
||||
@IWorkspaceContextService private readonly workspaceContext: IWorkspaceContextService,
|
||||
) {
|
||||
super()
|
||||
|
||||
const initializeURI = (uri: URI) => {
|
||||
this.workspaceContext.getWorkspace()
|
||||
const voidRulesURI = URI.joinPath(uri, '.voidrules')
|
||||
this.voidModelService.initializeModel(voidRulesURI)
|
||||
}
|
||||
|
||||
// call
|
||||
this._register(this.workspaceContext.onDidChangeWorkspaceFolders((e) => {
|
||||
[...e.changed, ...e.added].forEach(w => { initializeURI(w.uri) })
|
||||
}))
|
||||
this.workspaceContext.getWorkspace().folders.forEach(w => { initializeURI(w.uri) })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
registerWorkbenchContribution2(ConvertContribWorkbenchContribution.ID, ConvertContribWorkbenchContribution, WorkbenchPhase.BlockRestore);
|
||||
@@ -0,0 +1,428 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { IFileService } from '../../../../platform/files/common/files.js';
|
||||
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
|
||||
import { ShallowDirectoryItem, ToolCallParams, ToolResultType } from '../common/toolsServiceTypes.js';
|
||||
import { IExplorerService } from '../../files/browser/files.js';
|
||||
import { SortOrder } from '../../files/common/files.js';
|
||||
import { ExplorerItem } from '../../files/common/explorerModel.js';
|
||||
import { MAX_CHILDREN_URIs_PAGE, MAX_DIRSTR_CHARS_TOTAL_BEGINNING, MAX_DIRSTR_CHARS_TOTAL_TOOL } from '../common/prompt/prompts.js';
|
||||
|
||||
|
||||
const MAX_FILES_TOTAL = 300;
|
||||
|
||||
const DEFAULT_MAX_DEPTH = 3;
|
||||
const DEFAULT_MAX_ITEMS_PER_DIR = 3;
|
||||
|
||||
const START_MAX_DEPTH = Infinity;
|
||||
const START_MAX_ITEMS_PER_DIR = Infinity; // Add start value as Infinity
|
||||
|
||||
|
||||
export interface IDirectoryStrService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
getDirectoryStrTool(uri: URI, options?: { maxItemsPerDir?: number }): Promise<string>
|
||||
getAllDirectoriesStr(opts: { cutOffMessage: string, maxItemsPerDir?: number }): Promise<string>
|
||||
|
||||
}
|
||||
export const IDirectoryStrService = createDecorator<IDirectoryStrService>('voidDirectoryStrService');
|
||||
|
||||
|
||||
|
||||
|
||||
// Check if it's a known filtered type like .git
|
||||
const shouldExcludeDirectory = (item: ExplorerItem) => {
|
||||
if (item.name === '.git' ||
|
||||
item.name === 'node_modules' ||
|
||||
item.name.startsWith('.') ||
|
||||
item.name === 'dist' ||
|
||||
item.name === 'build' ||
|
||||
item.name === 'out' ||
|
||||
item.name === 'bin' ||
|
||||
item.name === 'coverage' ||
|
||||
item.name === '__pycache__' ||
|
||||
item.name === 'env' ||
|
||||
item.name === 'venv' ||
|
||||
item.name === 'tmp' ||
|
||||
item.name === 'temp' ||
|
||||
item.name === 'artifacts' ||
|
||||
item.name === 'target' ||
|
||||
item.name === 'obj' ||
|
||||
item.name === 'vendor' ||
|
||||
item.name === 'logs' ||
|
||||
item.name === 'cache' ||
|
||||
item.name === 'resource' ||
|
||||
item.name === 'resources'
|
||||
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (item.name.match(/\bout\b/)) return true
|
||||
if (item.name.match(/\bbuild\b/)) return true
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---------- ONE LAYER DEEP ----------
|
||||
|
||||
export const computeDirectoryTree1Deep = async (
|
||||
fileService: IFileService,
|
||||
rootURI: URI,
|
||||
pageNumber: number = 1,
|
||||
): Promise<ToolResultType['ls_dir']> => {
|
||||
const stat = await fileService.resolve(rootURI, { resolveMetadata: false });
|
||||
if (!stat.isDirectory) {
|
||||
return { children: null, hasNextPage: false, hasPrevPage: false, itemsRemaining: 0 };
|
||||
}
|
||||
|
||||
const nChildren = stat.children?.length ?? 0;
|
||||
|
||||
const fromChildIdx = MAX_CHILDREN_URIs_PAGE * (pageNumber - 1);
|
||||
const toChildIdx = MAX_CHILDREN_URIs_PAGE * pageNumber - 1; // INCLUSIVE
|
||||
const listChildren = stat.children?.slice(fromChildIdx, toChildIdx + 1);
|
||||
|
||||
const children: ShallowDirectoryItem[] = listChildren?.map(child => ({
|
||||
name: child.name,
|
||||
uri: child.resource,
|
||||
isDirectory: child.isDirectory,
|
||||
isSymbolicLink: child.isSymbolicLink
|
||||
})) ?? [];
|
||||
|
||||
const hasNextPage = (nChildren - 1) > toChildIdx;
|
||||
const hasPrevPage = pageNumber > 1;
|
||||
const itemsRemaining = Math.max(0, nChildren - (toChildIdx + 1));
|
||||
|
||||
return {
|
||||
children,
|
||||
hasNextPage,
|
||||
hasPrevPage,
|
||||
itemsRemaining
|
||||
};
|
||||
};
|
||||
|
||||
export const stringifyDirectoryTree1Deep = (params: ToolCallParams['ls_dir'], result: ToolResultType['ls_dir']): string => {
|
||||
if (!result.children) {
|
||||
return `Error: ${params.uri} is not a directory`;
|
||||
}
|
||||
|
||||
let output = '';
|
||||
const entries = result.children;
|
||||
|
||||
if (!result.hasPrevPage) { // is first page
|
||||
output += `${params.uri.fsPath}\n`;
|
||||
}
|
||||
|
||||
for (let i = 0; i < entries.length; i++) {
|
||||
const entry = entries[i];
|
||||
const isLast = i === entries.length - 1 && !result.hasNextPage;
|
||||
const prefix = isLast ? '└── ' : '├── ';
|
||||
|
||||
output += `${prefix}${entry.name}${entry.isDirectory ? '/' : ''}${entry.isSymbolicLink ? ' (symbolic link)' : ''}\n`;
|
||||
}
|
||||
|
||||
if (result.hasNextPage) {
|
||||
output += `└── (${result.itemsRemaining} results remaining...)\n`;
|
||||
}
|
||||
|
||||
return output;
|
||||
};
|
||||
|
||||
|
||||
// ---------- IN GENERAL ----------
|
||||
|
||||
// Remove the old computeDirectoryTree function and replace with a combined version that handles both computation and rendering
|
||||
const computeAndStringifyDirectoryTree = async (
|
||||
eItem: ExplorerItem,
|
||||
explorerService: IExplorerService,
|
||||
MAX_CHARS: number,
|
||||
fileCount: { count: number } = { count: 0 },
|
||||
options: { maxDepth?: number, currentDepth?: number, maxItemsPerDir?: number } = {}
|
||||
): Promise<{ content: string, wasCutOff: boolean }> => {
|
||||
// Set default values for options
|
||||
const maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;
|
||||
const currentDepth = options.currentDepth ?? 0;
|
||||
const maxItemsPerDir = options.maxItemsPerDir ?? DEFAULT_MAX_ITEMS_PER_DIR;
|
||||
|
||||
// Check if we've reached the max depth
|
||||
if (currentDepth > maxDepth) {
|
||||
return { content: '', wasCutOff: true };
|
||||
}
|
||||
|
||||
// Check if we've reached the file limit
|
||||
if (fileCount.count >= MAX_FILES_TOTAL) {
|
||||
return { content: '', wasCutOff: true };
|
||||
}
|
||||
|
||||
// If we're already exceeding the max characters, return immediately
|
||||
if (MAX_CHARS <= 0) {
|
||||
return { content: '', wasCutOff: true };
|
||||
}
|
||||
|
||||
// Increment file count
|
||||
fileCount.count++;
|
||||
|
||||
// Add the root node first (without tree characters)
|
||||
const nodeLine = `${eItem.name}${eItem.isDirectory ? '/' : ''}${eItem.isSymbolicLink ? ' (symbolic link)' : ''}\n`;
|
||||
|
||||
if (nodeLine.length > MAX_CHARS) {
|
||||
return { content: '', wasCutOff: true };
|
||||
}
|
||||
|
||||
let content = nodeLine;
|
||||
let wasCutOff = false;
|
||||
let remainingChars = MAX_CHARS - nodeLine.length;
|
||||
|
||||
// Check if it's a directory we should skip
|
||||
const isGitIgnoredDirectory = eItem.isDirectory && shouldExcludeDirectory(eItem);
|
||||
|
||||
// Fetch and process children if not a filtered directory
|
||||
if (eItem.isDirectory && !isGitIgnoredDirectory) {
|
||||
// Fetch children with Modified sort order to show recently modified first
|
||||
const eChildren = await eItem.fetchChildren(SortOrder.Modified);
|
||||
|
||||
// Then recursively add all children with proper tree formatting
|
||||
if (eChildren && eChildren.length > 0) {
|
||||
const { childrenContent, childrenCutOff } = await renderChildrenCombined(
|
||||
eChildren,
|
||||
remainingChars,
|
||||
'',
|
||||
explorerService,
|
||||
fileCount,
|
||||
{ maxDepth, currentDepth, maxItemsPerDir } // Pass maxItemsPerDir to the render function
|
||||
);
|
||||
content += childrenContent;
|
||||
wasCutOff = childrenCutOff;
|
||||
}
|
||||
}
|
||||
|
||||
return { content, wasCutOff };
|
||||
};
|
||||
|
||||
// Helper function to render children with proper tree formatting
|
||||
const renderChildrenCombined = async (
|
||||
children: ExplorerItem[],
|
||||
maxChars: number,
|
||||
parentPrefix: string,
|
||||
explorerService: IExplorerService,
|
||||
fileCount: { count: number },
|
||||
options: { maxDepth: number, currentDepth: number, maxItemsPerDir?: number }
|
||||
): Promise<{ childrenContent: string, childrenCutOff: boolean }> => {
|
||||
const { maxDepth, currentDepth } = options; // Remove maxItemsPerDir from destructuring
|
||||
// Get maxItemsPerDir separately and make sure we use it
|
||||
// For first level (currentDepth = 0), always use Infinity regardless of what was passed
|
||||
const maxItemsPerDir = currentDepth === 0 ?
|
||||
Infinity :
|
||||
(options.maxItemsPerDir ?? DEFAULT_MAX_ITEMS_PER_DIR);
|
||||
const nextDepth = currentDepth + 1;
|
||||
|
||||
let childrenContent = '';
|
||||
let childrenCutOff = false;
|
||||
let remainingChars = maxChars;
|
||||
|
||||
// Check if we've reached max depth
|
||||
if (nextDepth > maxDepth) {
|
||||
return { childrenContent: '', childrenCutOff: true };
|
||||
}
|
||||
|
||||
// Apply maxItemsPerDir limit - only process the specified number of items
|
||||
const itemsToProcess = maxItemsPerDir === Infinity ? children : children.slice(0, maxItemsPerDir);
|
||||
const hasMoreItems = children.length > itemsToProcess.length;
|
||||
|
||||
for (let i = 0; i < itemsToProcess.length; i++) {
|
||||
// Check if we've reached the file limit
|
||||
if (fileCount.count >= MAX_FILES_TOTAL) {
|
||||
childrenCutOff = true;
|
||||
break;
|
||||
}
|
||||
|
||||
const child = itemsToProcess[i];
|
||||
const isLast = (i === itemsToProcess.length - 1) && !hasMoreItems;
|
||||
|
||||
// Create the tree branch symbols
|
||||
const branchSymbol = isLast ? '└── ' : '├── ';
|
||||
const childLine = `${parentPrefix}${branchSymbol}${child.name}${child.isDirectory ? '/' : ''}${child.isSymbolicLink ? ' (symbolic link)' : ''}\n`;
|
||||
|
||||
// Check if adding this line would exceed the limit
|
||||
if (childLine.length > remainingChars) {
|
||||
childrenCutOff = true;
|
||||
break;
|
||||
}
|
||||
|
||||
childrenContent += childLine;
|
||||
remainingChars -= childLine.length;
|
||||
fileCount.count++;
|
||||
|
||||
const nextLevelPrefix = parentPrefix + (isLast ? ' ' : '│ ');
|
||||
|
||||
// Skip processing children for git ignored directories
|
||||
const isGitIgnoredDirectory = child.isDirectory && shouldExcludeDirectory(child);
|
||||
|
||||
// Create the prefix for the next level (continuation line or space)
|
||||
if (child.isDirectory && !isGitIgnoredDirectory) {
|
||||
// Fetch children with Modified sort order to show recently modified first
|
||||
const eChildren = await child.fetchChildren(SortOrder.Modified);
|
||||
|
||||
if (eChildren && eChildren.length > 0) {
|
||||
const {
|
||||
childrenContent: grandChildrenContent,
|
||||
childrenCutOff: grandChildrenCutOff
|
||||
} = await renderChildrenCombined(
|
||||
eChildren,
|
||||
remainingChars,
|
||||
nextLevelPrefix,
|
||||
explorerService,
|
||||
fileCount,
|
||||
{ maxDepth, currentDepth: nextDepth, maxItemsPerDir }
|
||||
);
|
||||
|
||||
if (grandChildrenContent.length > 0) {
|
||||
childrenContent += grandChildrenContent;
|
||||
remainingChars -= grandChildrenContent.length;
|
||||
}
|
||||
|
||||
if (grandChildrenCutOff) {
|
||||
childrenCutOff = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add a message if we truncated the items due to maxItemsPerDir
|
||||
if (hasMoreItems) {
|
||||
const remainingCount = children.length - itemsToProcess.length;
|
||||
const truncatedLine = `${parentPrefix}└── (${remainingCount} more items not shown...)\n`;
|
||||
|
||||
if (truncatedLine.length <= remainingChars) {
|
||||
childrenContent += truncatedLine;
|
||||
remainingChars -= truncatedLine.length;
|
||||
}
|
||||
childrenCutOff = true;
|
||||
}
|
||||
|
||||
return { childrenContent, childrenCutOff };
|
||||
};
|
||||
|
||||
|
||||
// ---------------------------------------------------
|
||||
|
||||
|
||||
class DirectoryStrService extends Disposable implements IDirectoryStrService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
constructor(
|
||||
@IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService,
|
||||
@IExplorerService private readonly explorerService: IExplorerService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async getDirectoryStrTool(uri: URI, options?: { maxItemsPerDir?: number }) {
|
||||
const eRoot = this.explorerService.findClosest(uri)
|
||||
if (!eRoot) throw new Error(`The folder ${uri.fsPath} does not exist.`)
|
||||
|
||||
const maxItemsPerDir = options?.maxItemsPerDir ?? START_MAX_ITEMS_PER_DIR; // Use START_MAX_ITEMS_PER_DIR
|
||||
|
||||
// First try with START_MAX_DEPTH
|
||||
const { content: initialContent, wasCutOff: initialCutOff } = await computeAndStringifyDirectoryTree(
|
||||
eRoot,
|
||||
this.explorerService,
|
||||
MAX_DIRSTR_CHARS_TOTAL_TOOL,
|
||||
{ count: 0 },
|
||||
{ maxDepth: START_MAX_DEPTH, currentDepth: 0, maxItemsPerDir }
|
||||
);
|
||||
|
||||
// If cut off, try again with DEFAULT_MAX_DEPTH and DEFAULT_MAX_ITEMS_PER_DIR
|
||||
let content, wasCutOff;
|
||||
if (initialCutOff) {
|
||||
const result = await computeAndStringifyDirectoryTree(
|
||||
eRoot,
|
||||
this.explorerService,
|
||||
MAX_DIRSTR_CHARS_TOTAL_TOOL,
|
||||
{ count: 0 },
|
||||
{ maxDepth: DEFAULT_MAX_DEPTH, currentDepth: 0, maxItemsPerDir: DEFAULT_MAX_ITEMS_PER_DIR }
|
||||
);
|
||||
content = result.content;
|
||||
wasCutOff = result.wasCutOff;
|
||||
} else {
|
||||
content = initialContent;
|
||||
wasCutOff = initialCutOff;
|
||||
}
|
||||
|
||||
let c = content.substring(0, MAX_DIRSTR_CHARS_TOTAL_TOOL)
|
||||
c = `Directory of ${uri.fsPath}:\n${content}`
|
||||
if (wasCutOff) c = `${c}\n...Result was truncated...`
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
async getAllDirectoriesStr({ cutOffMessage, maxItemsPerDir }: { cutOffMessage: string, maxItemsPerDir?: number }) {
|
||||
let str: string = '';
|
||||
let cutOff = false;
|
||||
const folders = this.workspaceContextService.getWorkspace().folders;
|
||||
if (folders.length === 0)
|
||||
return '(NO WORKSPACE OPEN)';
|
||||
|
||||
// Use START_MAX_ITEMS_PER_DIR if not specified
|
||||
const startMaxItemsPerDir = maxItemsPerDir ?? START_MAX_ITEMS_PER_DIR;
|
||||
|
||||
for (let i = 0; i < folders.length; i += 1) {
|
||||
if (i > 0) str += '\n';
|
||||
|
||||
// this prioritizes filling 1st workspace before any other, etc
|
||||
const f = folders[i];
|
||||
str += `Directory of ${f.uri.fsPath}:\n`;
|
||||
const rootURI = f.uri;
|
||||
|
||||
const eRoot = this.explorerService.findClosestRoot(rootURI);
|
||||
if (!eRoot) continue;
|
||||
|
||||
// First try with START_MAX_DEPTH and startMaxItemsPerDir
|
||||
const { content: initialContent, wasCutOff: initialCutOff } = await computeAndStringifyDirectoryTree(
|
||||
eRoot,
|
||||
this.explorerService,
|
||||
MAX_DIRSTR_CHARS_TOTAL_BEGINNING - str.length,
|
||||
{ count: 0 },
|
||||
{ maxDepth: START_MAX_DEPTH, currentDepth: 0, maxItemsPerDir: startMaxItemsPerDir }
|
||||
);
|
||||
|
||||
// If cut off, try again with DEFAULT_MAX_DEPTH and DEFAULT_MAX_ITEMS_PER_DIR
|
||||
let content, wasCutOff;
|
||||
if (initialCutOff) {
|
||||
const result = await computeAndStringifyDirectoryTree(
|
||||
eRoot,
|
||||
this.explorerService,
|
||||
MAX_DIRSTR_CHARS_TOTAL_BEGINNING - str.length,
|
||||
{ count: 0 },
|
||||
{ maxDepth: DEFAULT_MAX_DEPTH, currentDepth: 0, maxItemsPerDir: DEFAULT_MAX_ITEMS_PER_DIR }
|
||||
);
|
||||
content = result.content;
|
||||
wasCutOff = result.wasCutOff;
|
||||
} else {
|
||||
content = initialContent;
|
||||
wasCutOff = initialCutOff;
|
||||
}
|
||||
|
||||
str += content;
|
||||
if (wasCutOff) {
|
||||
cutOff = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (cutOff) {
|
||||
return `${str.trimEnd()}\n${cutOffMessage}`
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
}
|
||||
|
||||
registerSingleton(IDirectoryStrService, DirectoryStrService, InstantiationType.Delayed);
|
||||
@@ -11,7 +11,7 @@ import { ICodeEditor, IOverlayWidget, IViewZone } from '../../../../editor/brows
|
||||
// import { IUndoRedoService } from '../../../../platform/undoRedo/common/undoRedo.js';
|
||||
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
|
||||
// import { throttle } from '../../../../base/common/decorators.js';
|
||||
import { ComputedDiff, findDiffs } from './helpers/findDiffs.js';
|
||||
import { findDiffs } from './helpers/findDiffs.js';
|
||||
import { EndOfLinePreference, IModelDecorationOptions, ITextModel } from '../../../../editor/common/model.js';
|
||||
import { IRange } from '../../../../editor/common/core/range.js';
|
||||
import { registerColor } from '../../../../platform/theme/common/colorUtils.js';
|
||||
@@ -31,22 +31,22 @@ import { mountCtrlK } from './react/out/quick-edit-tsx/index.js'
|
||||
import { QuickEditPropsType } from './quickEditActions.js';
|
||||
import { IModelContentChangedEvent } from '../../../../editor/common/textModelEvents.js';
|
||||
import { extractCodeFromFIM, extractCodeFromRegular, ExtractedSearchReplaceBlock, extractSearchReplaceBlocks } from '../common/helpers/extractCodeFromResult.js';
|
||||
import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js';
|
||||
import { isMacintosh } from '../../../../base/common/platform.js';
|
||||
import { INotificationService, } from '../../../../platform/notification/common/notification.js';
|
||||
import { EditorOption } from '../../../../editor/common/config/editorOptions.js';
|
||||
import { Emitter } from '../../../../base/common/event.js';
|
||||
import { VOID_OPEN_SETTINGS_ACTION_ID } from './voidSettingsPane.js';
|
||||
import { ICommandService } from '../../../../platform/commands/common/commands.js';
|
||||
import { ILLMMessageService } from '../common/sendLLMMessageService.js';
|
||||
import { LLMChatMessage, OnError, errorDetails } from '../common/sendLLMMessageTypes.js';
|
||||
import { LLMChatMessage } from '../common/sendLLMMessageTypes.js';
|
||||
import { IMetricsService } from '../common/metricsService.js';
|
||||
import { IEditCodeService, AddCtrlKOpts, StartApplyingOpts } from './editCodeServiceInterface.js';
|
||||
import { IEditCodeService, AddCtrlKOpts, StartApplyingOpts, CallBeforeStartApplyingOpts, } from './editCodeServiceInterface.js';
|
||||
import { IVoidSettingsService } from '../common/voidSettingsService.js';
|
||||
import { FeatureName } from '../common/voidSettingsTypes.js';
|
||||
import { IVoidModelService } from '../common/voidModelService.js';
|
||||
import { ITextFileService } from '../../../services/textfile/common/textfiles.js';
|
||||
import { deepClone } from '../../../../base/common/objects.js';
|
||||
import { acceptBg, acceptBorder, buttonFontSize, buttonTextColor, rejectBg, rejectBorder } from '../common/helpers/colors.js';
|
||||
import { DiffArea, Diff, CtrlKZone, VoidFileSnapshot, DiffAreaSnapshotEntry, diffAreaSnapshotKeys, DiffZone, TrackingZone, ComputedDiff } from '../common/editCodeServiceTypes.js';
|
||||
import { IConvertToLLMMessageService } from './convertToLLMMessageService.js';
|
||||
// import { isMacintosh } from '../../../../base/common/platform.js';
|
||||
// import { VOID_OPEN_SETTINGS_ACTION_ID } from './voidSettingsPane.js';
|
||||
|
||||
const configOfBG = (color: Color) => {
|
||||
return { dark: color, light: color, hcDark: color, hcLight: color, }
|
||||
@@ -71,6 +71,21 @@ registerColor('void.sweepIdxBG', configOfBG(sweepIdxBG), '', true);
|
||||
|
||||
const numLinesOfStr = (str: string) => str.split('\n').length
|
||||
|
||||
|
||||
export const getLengthOfTextPx = ({ tabWidth, spaceWidth, content }: { tabWidth: number, spaceWidth: number, content: string }) => {
|
||||
let lengthOfTextPx = 0;
|
||||
for (const char of content) {
|
||||
if (char === '\t') {
|
||||
lengthOfTextPx += tabWidth
|
||||
} else {
|
||||
lengthOfTextPx += spaceWidth;
|
||||
}
|
||||
}
|
||||
|
||||
return lengthOfTextPx
|
||||
}
|
||||
|
||||
|
||||
const getLeadingWhitespacePx = (editor: ICodeEditor, startLine: number): number => {
|
||||
|
||||
const model = editor.getModel();
|
||||
@@ -94,28 +109,42 @@ const getLeadingWhitespacePx = (editor: ICodeEditor, startLine: number): number
|
||||
const spaceWidth = editor.getOption(EditorOption.fontInfo).spaceWidth;
|
||||
const tabWidth = numSpacesInTab * spaceWidth;
|
||||
|
||||
let paddingLeft = 0;
|
||||
for (const char of leadingWhitespace) {
|
||||
if (char === '\t') {
|
||||
paddingLeft += tabWidth
|
||||
} else if (char === ' ') {
|
||||
paddingLeft += spaceWidth;
|
||||
}
|
||||
}
|
||||
const leftWhitespacePx = getLengthOfTextPx({
|
||||
tabWidth,
|
||||
spaceWidth,
|
||||
content: leadingWhitespace
|
||||
});
|
||||
|
||||
return paddingLeft;
|
||||
|
||||
return leftWhitespacePx;
|
||||
};
|
||||
|
||||
|
||||
// Helper function to remove whitespace except newlines
|
||||
const removeWhitespaceExceptNewlines = (str: string): string => {
|
||||
return str.replace(/[^\S\n]+/g, '');
|
||||
}
|
||||
|
||||
|
||||
|
||||
// finds block.orig in fileContents and return its range in file
|
||||
// startingAtLine is 1-indexed and inclusive
|
||||
const findTextInCode = (text: string, fileContents: string, startingAtLine?: number) => {
|
||||
const idx = fileContents.indexOf(text,
|
||||
startingAtLine !== undefined ?
|
||||
fileContents.split('\n').slice(0, startingAtLine).join('\n').length // num characters in all lines before startingAtLine
|
||||
: 0
|
||||
)
|
||||
const findTextInCode = (text: string, fileContents: string, canFallbackToRemoveWhitespace: boolean, startingAtLine?: number) => {
|
||||
|
||||
const startLineIdx = (fileContents: string) => startingAtLine !== undefined ?
|
||||
fileContents.split('\n').slice(0, startingAtLine).join('\n').length // num characters in all lines before startingAtLine
|
||||
: 0
|
||||
|
||||
// idx = starting index in fileContents
|
||||
let idx = fileContents.indexOf(text, startLineIdx(fileContents))
|
||||
|
||||
// try to find it ignoring all whitespace this time
|
||||
if (idx === -1 && canFallbackToRemoveWhitespace) {
|
||||
text = removeWhitespaceExceptNewlines(text)
|
||||
fileContents = removeWhitespaceExceptNewlines(fileContents)
|
||||
idx = fileContents.indexOf(text, startLineIdx(fileContents));
|
||||
}
|
||||
|
||||
if (idx === -1) return 'Not found' as const
|
||||
const lastIdx = fileContents.lastIndexOf(text)
|
||||
if (lastIdx !== idx) return 'Not unique' as const
|
||||
@@ -127,108 +156,6 @@ const findTextInCode = (text: string, fileContents: string, startingAtLine?: num
|
||||
|
||||
|
||||
|
||||
|
||||
// // TODO diffArea should be removed if we just discovered it has no more diffs in it
|
||||
// for (const diffareaid of this.diffAreasOfURI[uri.fsPath] || []) {
|
||||
// const diffArea = this.diffAreaOfId[diffareaid]
|
||||
// if (Object.keys(diffArea._diffOfId).length === 0 && !diffArea._sweepState.isStreaming) {
|
||||
// const { onFinishEdit } = this._addToHistory(uri)
|
||||
// this._deleteDiffArea(diffArea)
|
||||
// onFinishEdit()
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
export type Diff = {
|
||||
diffid: number;
|
||||
diffareaid: number; // the diff area this diff belongs to, "computed"
|
||||
} & ComputedDiff
|
||||
|
||||
|
||||
|
||||
// _ means anything we don't include if we clone it
|
||||
// DiffArea.originalStartLine is the line in originalCode (not the file)
|
||||
|
||||
type CommonZoneProps = {
|
||||
diffareaid: number;
|
||||
startLine: number;
|
||||
endLine: number;
|
||||
|
||||
_URI: URI; // typically we get the URI from model
|
||||
|
||||
}
|
||||
|
||||
type CtrlKZone = {
|
||||
type: 'CtrlKZone';
|
||||
originalCode?: undefined;
|
||||
|
||||
editorId: string; // the editor the input lives on
|
||||
|
||||
_mountInfo: null | {
|
||||
textAreaRef: { current: HTMLTextAreaElement | null }
|
||||
dispose: () => void;
|
||||
refresh: () => void;
|
||||
}
|
||||
|
||||
_linkedStreamingDiffZone: number | null; // diffareaid of the diffZone currently streaming here
|
||||
_removeStylesFns: Set<Function> // these don't remove diffs or this diffArea, only their styles
|
||||
|
||||
} & CommonZoneProps
|
||||
|
||||
|
||||
export type DiffZone = {
|
||||
type: 'DiffZone',
|
||||
originalCode: string;
|
||||
_diffOfId: Record<string, Diff>; // diffid -> diff in this DiffArea
|
||||
_streamState: {
|
||||
isStreaming: true;
|
||||
streamRequestIdRef: { current: string | null };
|
||||
line: number;
|
||||
} | {
|
||||
isStreaming: false;
|
||||
streamRequestIdRef?: undefined;
|
||||
line?: undefined;
|
||||
};
|
||||
editorId?: undefined;
|
||||
linkedStreamingDiffZone?: undefined;
|
||||
_removeStylesFns: Set<Function> // these don't remove diffs or this diffArea, only their styles
|
||||
} & CommonZoneProps
|
||||
|
||||
|
||||
|
||||
type TrackingZone<T> = {
|
||||
type: 'TrackingZone';
|
||||
metadata: T;
|
||||
originalCode?: undefined;
|
||||
editorId?: undefined;
|
||||
_removeStylesFns?: undefined;
|
||||
} & CommonZoneProps
|
||||
|
||||
|
||||
// called DiffArea for historical purposes, we can rename to something like TextRegion if we want
|
||||
export type DiffArea = CtrlKZone | DiffZone | TrackingZone<any>
|
||||
|
||||
const diffAreaSnapshotKeys = [
|
||||
'type',
|
||||
'diffareaid',
|
||||
'originalCode',
|
||||
'startLine',
|
||||
'endLine',
|
||||
'editorId',
|
||||
|
||||
] as const satisfies (keyof DiffArea)[]
|
||||
|
||||
type DiffAreaSnapshot<DiffAreaType extends DiffArea = DiffArea> = Pick<DiffAreaType, typeof diffAreaSnapshotKeys[number]>
|
||||
|
||||
|
||||
|
||||
type HistorySnapshot = {
|
||||
snapshottedDiffAreaOfId: Record<string, DiffAreaSnapshot>;
|
||||
entireFileCode: string;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// line/col is the location, originalCodeStartLine is the start line of the original code being displayed
|
||||
type StreamLocationMutable = { line: number, col: number, addedSplitYet: boolean, originalCodeStartLine: number }
|
||||
|
||||
@@ -243,23 +170,21 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
diffAreaOfId: Record<string, DiffArea> = {}; // diffareaId -> diffArea
|
||||
diffOfId: Record<string, Diff> = {}; // diffid -> diff (redundant with diffArea._diffOfId)
|
||||
|
||||
|
||||
// events
|
||||
|
||||
|
||||
// uri: diffZones // listen on change diffZones
|
||||
private readonly _onDidAddOrDeleteDiffZones = new Emitter<{ uri: URI }>();
|
||||
onDidAddOrDeleteDiffZones = this._onDidAddOrDeleteDiffZones.event;
|
||||
|
||||
// diffZone: [uri], diffs, isStreaming // listen on change diffs, change streaming (uri is const)
|
||||
private readonly _onDidChangeDiffsInDiffZone = new Emitter<{ uri: URI, diffareaid: number }>();
|
||||
private readonly _onDidChangeDiffsInDiffZoneNotStreaming = new Emitter<{ uri: URI, diffareaid: number }>();
|
||||
private readonly _onDidChangeStreamingInDiffZone = new Emitter<{ uri: URI, diffareaid: number }>();
|
||||
onDidChangeDiffsInDiffZone = this._onDidChangeDiffsInDiffZone.event;
|
||||
onDidChangeDiffsInDiffZoneNotStreaming = this._onDidChangeDiffsInDiffZoneNotStreaming.event;
|
||||
onDidChangeStreamingInDiffZone = this._onDidChangeStreamingInDiffZone.event;
|
||||
|
||||
// ctrlKZone: [uri], isStreaming // listen on change streaming
|
||||
private readonly _onDidChangeStreamingInCtrlKZone = new Emitter<{ uri: URI; diffareaid: number }>();
|
||||
onDidChangeStreamingInCtrlKZone = this._onDidChangeStreamingInCtrlKZone.event
|
||||
onDidChangeStreamingInCtrlKZone = this._onDidChangeStreamingInCtrlKZone.event;
|
||||
|
||||
|
||||
constructor(
|
||||
@@ -273,11 +198,11 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
@IConsistentEditorItemService private readonly _consistentEditorItemService: IConsistentEditorItemService,
|
||||
@IMetricsService private readonly _metricsService: IMetricsService,
|
||||
@INotificationService private readonly _notificationService: INotificationService,
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
// @ICommandService private readonly _commandService: ICommandService,
|
||||
@IVoidSettingsService private readonly _settingsService: IVoidSettingsService,
|
||||
// @IFileService private readonly _fileService: IFileService,
|
||||
@IVoidModelService private readonly _voidModelService: IVoidModelService,
|
||||
@ITextFileService private readonly _textFileService: ITextFileService,
|
||||
@IConvertToLLMMessageService private readonly _convertToLLMMessageService: IConvertToLLMMessageService,
|
||||
) {
|
||||
super();
|
||||
|
||||
@@ -332,29 +257,45 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
this._realignAllDiffAreasLines(uri, change.text, change.range)
|
||||
}
|
||||
this._refreshStylesAndDiffsInURI(uri)
|
||||
|
||||
// if diffarea has no diffs after a user edit, delete it
|
||||
const diffAreasToDelete: DiffZone[] = []
|
||||
for (const diffareaid of this.diffAreasOfURI[uri.fsPath] ?? []) {
|
||||
const diffArea = this.diffAreaOfId[diffareaid] ?? null
|
||||
const shouldDelete = diffArea?.type === 'DiffZone' && Object.keys(diffArea._diffOfId).length === 0
|
||||
if (shouldDelete) {
|
||||
diffAreasToDelete.push(diffArea)
|
||||
}
|
||||
}
|
||||
if (diffAreasToDelete.length !== 0) {
|
||||
const { onFinishEdit } = this._addToHistory(uri)
|
||||
diffAreasToDelete.forEach(da => this._deleteDiffZone(da))
|
||||
onFinishEdit()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
private _notifyError = (e: Parameters<OnError>[0]) => {
|
||||
const details = errorDetails(e.fullError)
|
||||
this._notificationService.notify({
|
||||
severity: Severity.Warning,
|
||||
message: `Void Error: ${e.message}`,
|
||||
actions: {
|
||||
secondary: [{
|
||||
id: 'void.onerror.opensettings',
|
||||
enabled: true,
|
||||
label: `Open Void's settings`,
|
||||
tooltip: '',
|
||||
class: undefined,
|
||||
run: () => { this._commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID) }
|
||||
}]
|
||||
},
|
||||
source: details ? `(Hold ${isMacintosh ? 'Option' : 'Alt'} to hover) - ${details}\n\nIf this persists, feel free to [report](https://github.com/voideditor/void/issues/new) it.` : undefined
|
||||
})
|
||||
}
|
||||
// private _notifyError = (e: Parameters<OnError>[0]) => {
|
||||
// const details = errorDetails(e.fullError)
|
||||
// this._notificationService.notify({
|
||||
// severity: Severity.Warning,
|
||||
// message: `Void Error: ${e.message}`,
|
||||
// actions: {
|
||||
// secondary: [{
|
||||
// id: 'void.onerror.opensettings',
|
||||
// enabled: true,
|
||||
// label: `Open Void's settings`,
|
||||
// tooltip: '',
|
||||
// class: undefined,
|
||||
// run: () => { this._commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID) }
|
||||
// }]
|
||||
// },
|
||||
// source: details ? `(Hold ${isMacintosh ? 'Option' : 'Alt'} to hover) - ${details}\n\nIf this persists, feel free to [report](https://github.com/voideditor/void/issues/new) it.` : undefined
|
||||
// })
|
||||
// }
|
||||
|
||||
|
||||
|
||||
@@ -722,106 +663,114 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
private _getCurrentVoidFileSnapshot = (uri: URI): VoidFileSnapshot => {
|
||||
const { model } = this._voidModelService.getModel(uri)
|
||||
const snapshottedDiffAreaOfId: Record<string, DiffAreaSnapshotEntry> = {}
|
||||
|
||||
for (const diffareaid in this.diffAreaOfId) {
|
||||
const diffArea = this.diffAreaOfId[diffareaid]
|
||||
|
||||
if (diffArea._URI.fsPath !== uri.fsPath) continue
|
||||
|
||||
snapshottedDiffAreaOfId[diffareaid] = deepClone(
|
||||
Object.fromEntries(diffAreaSnapshotKeys.map(key => [key, diffArea[key]]))
|
||||
) as DiffAreaSnapshotEntry
|
||||
}
|
||||
|
||||
const entireFileCode = model ? model.getValue(EndOfLinePreference.LF) : ''
|
||||
|
||||
// this._noLongerNeedModelReference(uri)
|
||||
return {
|
||||
snapshottedDiffAreaOfId,
|
||||
entireFileCode, // the whole file's code
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private _restoreVoidFileSnapshot = async (uri: URI, snapshot: VoidFileSnapshot) => {
|
||||
// for each diffarea in this uri, stop streaming if currently streaming
|
||||
for (const diffareaid in this.diffAreaOfId) {
|
||||
const diffArea = this.diffAreaOfId[diffareaid]
|
||||
if (diffArea.type === 'DiffZone')
|
||||
this._stopIfStreaming(diffArea)
|
||||
}
|
||||
|
||||
// delete all diffareas on this uri (clearing their styles)
|
||||
this._deleteAllDiffAreas(uri)
|
||||
|
||||
const { snapshottedDiffAreaOfId, entireFileCode: entireModelCode } = deepClone(snapshot) // don't want to destroy the snapshot
|
||||
|
||||
// restore diffAreaOfId and diffAreasOfModelId
|
||||
for (const diffareaid in snapshottedDiffAreaOfId) {
|
||||
|
||||
const snapshottedDiffArea = snapshottedDiffAreaOfId[diffareaid]
|
||||
|
||||
if (snapshottedDiffArea.type === 'DiffZone') {
|
||||
this.diffAreaOfId[diffareaid] = {
|
||||
...snapshottedDiffArea as DiffAreaSnapshotEntry<DiffZone>,
|
||||
type: 'DiffZone',
|
||||
_diffOfId: {},
|
||||
_URI: uri,
|
||||
_streamState: { isStreaming: false }, // when restoring, we will never be streaming
|
||||
_removeStylesFns: new Set(),
|
||||
}
|
||||
}
|
||||
else if (snapshottedDiffArea.type === 'CtrlKZone') {
|
||||
this.diffAreaOfId[diffareaid] = {
|
||||
...snapshottedDiffArea as DiffAreaSnapshotEntry<CtrlKZone>,
|
||||
_URI: uri,
|
||||
_removeStylesFns: new Set<Function>(),
|
||||
_mountInfo: null,
|
||||
_linkedStreamingDiffZone: null, // when restoring, we will never be streaming
|
||||
}
|
||||
}
|
||||
this._addOrInitializeDiffAreaAtURI(uri, diffareaid)
|
||||
}
|
||||
this._onDidAddOrDeleteDiffZones.fire({ uri })
|
||||
|
||||
// restore file content
|
||||
this._writeURIText(uri, entireModelCode,
|
||||
'wholeFileRange',
|
||||
{ shouldRealignDiffAreas: false }
|
||||
)
|
||||
// this._noLongerNeedModelReference(uri)
|
||||
}
|
||||
|
||||
private _addToHistory(uri: URI, opts?: { onWillUndo?: () => void }) {
|
||||
|
||||
const getCurrentSnapshot = (): HistorySnapshot => {
|
||||
|
||||
const { model } = this._voidModelService.getModel(uri)
|
||||
const snapshottedDiffAreaOfId: Record<string, DiffAreaSnapshot> = {}
|
||||
|
||||
for (const diffareaid in this.diffAreaOfId) {
|
||||
const diffArea = this.diffAreaOfId[diffareaid]
|
||||
|
||||
if (diffArea._URI.fsPath !== uri.fsPath) continue
|
||||
|
||||
snapshottedDiffAreaOfId[diffareaid] = deepClone(
|
||||
Object.fromEntries(diffAreaSnapshotKeys.map(key => [key, diffArea[key]]))
|
||||
) as DiffAreaSnapshot
|
||||
}
|
||||
|
||||
const entireFileCode = model ? model.getValue(EndOfLinePreference.LF) : ''
|
||||
|
||||
// this._noLongerNeedModelReference(uri)
|
||||
return {
|
||||
snapshottedDiffAreaOfId,
|
||||
entireFileCode, // the whole file's code
|
||||
}
|
||||
}
|
||||
|
||||
const restoreDiffAreas = async (snapshot: HistorySnapshot) => {
|
||||
|
||||
// for each diffarea in this uri, stop streaming if currently streaming
|
||||
for (const diffareaid in this.diffAreaOfId) {
|
||||
const diffArea = this.diffAreaOfId[diffareaid]
|
||||
if (diffArea.type === 'DiffZone')
|
||||
this._stopIfStreaming(diffArea)
|
||||
}
|
||||
|
||||
// delete all diffareas on this uri (clearing their styles)
|
||||
this._deleteAllDiffAreas(uri)
|
||||
this.diffAreasOfURI[uri.fsPath]?.clear()
|
||||
|
||||
const { snapshottedDiffAreaOfId, entireFileCode: entireModelCode } = deepClone(snapshot) // don't want to destroy the snapshot
|
||||
|
||||
// restore diffAreaOfId and diffAreasOfModelId
|
||||
for (const diffareaid in snapshottedDiffAreaOfId) {
|
||||
|
||||
const snapshottedDiffArea = snapshottedDiffAreaOfId[diffareaid]
|
||||
|
||||
if (snapshottedDiffArea.type === 'DiffZone') {
|
||||
this.diffAreaOfId[diffareaid] = {
|
||||
...snapshottedDiffArea as DiffAreaSnapshot<DiffZone>,
|
||||
type: 'DiffZone',
|
||||
_diffOfId: {},
|
||||
_URI: uri,
|
||||
_streamState: { isStreaming: false }, // when restoring, we will never be streaming
|
||||
_removeStylesFns: new Set(),
|
||||
}
|
||||
}
|
||||
else if (snapshottedDiffArea.type === 'CtrlKZone') {
|
||||
this.diffAreaOfId[diffareaid] = {
|
||||
...snapshottedDiffArea as DiffAreaSnapshot<CtrlKZone>,
|
||||
_URI: uri,
|
||||
_removeStylesFns: new Set<Function>(),
|
||||
_mountInfo: null,
|
||||
_linkedStreamingDiffZone: null, // when restoring, we will never be streaming
|
||||
}
|
||||
}
|
||||
this._addOrInitializeDiffAreaAtURI(uri, diffareaid)
|
||||
}
|
||||
this._onDidAddOrDeleteDiffZones.fire({ uri })
|
||||
|
||||
// restore file content
|
||||
this._writeURIText(uri, entireModelCode,
|
||||
'wholeFileRange',
|
||||
{ shouldRealignDiffAreas: false }
|
||||
)
|
||||
// this._noLongerNeedModelReference(uri)
|
||||
}
|
||||
|
||||
const beforeSnapshot: HistorySnapshot = getCurrentSnapshot()
|
||||
let afterSnapshot: HistorySnapshot | null = null
|
||||
const beforeSnapshot: VoidFileSnapshot = this._getCurrentVoidFileSnapshot(uri)
|
||||
let afterSnapshot: VoidFileSnapshot | null = null
|
||||
|
||||
const elt: IUndoRedoElement = {
|
||||
type: UndoRedoElementType.Resource,
|
||||
resource: uri,
|
||||
label: 'Void Agent',
|
||||
code: 'undoredo.editCode',
|
||||
undo: () => { opts?.onWillUndo?.(); restoreDiffAreas(beforeSnapshot); },
|
||||
redo: () => { if (afterSnapshot) restoreDiffAreas(afterSnapshot) }
|
||||
undo: async () => { opts?.onWillUndo?.(); await this._restoreVoidFileSnapshot(uri, beforeSnapshot) },
|
||||
redo: async () => { if (afterSnapshot) await this._restoreVoidFileSnapshot(uri, afterSnapshot) }
|
||||
}
|
||||
this._undoRedoService.pushElement(elt)
|
||||
|
||||
const onFinishEdit = async () => {
|
||||
afterSnapshot = getCurrentSnapshot()
|
||||
await this._textFileService.save(uri, { // we want [our change] -> [save] so it's all treated as one change.
|
||||
skipSaveParticipants: true // avoid triggering extensions etc (if they reformat the page, it will add another item to the undo stack)
|
||||
})
|
||||
afterSnapshot = this._getCurrentVoidFileSnapshot(uri)
|
||||
await this._voidModelService.saveModel(uri)
|
||||
}
|
||||
return { onFinishEdit }
|
||||
}
|
||||
|
||||
|
||||
public getVoidFileSnapshot(uri: URI) {
|
||||
return this._getCurrentVoidFileSnapshot(uri)
|
||||
}
|
||||
|
||||
|
||||
public restoreVoidFileSnapshot(uri: URI, snapshot: VoidFileSnapshot): void {
|
||||
this._restoreVoidFileSnapshot(uri, snapshot)
|
||||
}
|
||||
|
||||
|
||||
// delete diffOfId and diffArea._diffOfId
|
||||
private _deleteDiff(diff: Diff) {
|
||||
const diffArea = this.diffAreaOfId[diff.diffareaid]
|
||||
@@ -886,6 +835,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
else if (diffArea.type === 'CtrlKZone')
|
||||
this._deleteCtrlKZone(diffArea)
|
||||
})
|
||||
this.diffAreasOfURI[uri.fsPath]?.clear()
|
||||
}
|
||||
|
||||
private _addOrInitializeDiffAreaAtURI = (uri: URI, diffareaid: string | number) => {
|
||||
@@ -994,7 +944,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
if (diffArea?.type !== 'DiffZone') continue
|
||||
// fire changed diffs (this is the only place Diffs are added)
|
||||
if (!diffArea._streamState.isStreaming) {
|
||||
this._onDidChangeDiffsInDiffZone.fire({ uri, diffareaid: diffArea.diffareaid })
|
||||
this._onDidChangeDiffsInDiffZoneNotStreaming.fire({ uri, diffareaid: diffArea.diffareaid })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1160,22 +1110,51 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
|
||||
|
||||
|
||||
private _getURIBeforeStartApplying(opts: CallBeforeStartApplyingOpts) {
|
||||
// SR
|
||||
if (opts.from === 'ClickApply') {
|
||||
const uri = this._uriOfGivenURI(opts.uri)
|
||||
if (!uri) return
|
||||
return uri
|
||||
}
|
||||
else if (opts.from === 'QuickEdit') {
|
||||
const { diffareaid } = opts
|
||||
const ctrlKZone = this.diffAreaOfId[diffareaid]
|
||||
if (ctrlKZone?.type !== 'CtrlKZone') return
|
||||
const { _URI: uri } = ctrlKZone
|
||||
return uri
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
public async callBeforeStartApplying(opts: CallBeforeStartApplyingOpts) {
|
||||
const uri = this._getURIBeforeStartApplying(opts)
|
||||
if (!uri) return
|
||||
await this._voidModelService.initializeModel(uri)
|
||||
await this._voidModelService.saveModel(uri) // save the URI
|
||||
}
|
||||
|
||||
|
||||
// the applyDonePromise this returns can reject, and should be caught with .catch
|
||||
public async startApplying(opts: StartApplyingOpts): Promise<[URI, Promise<void>] | null> {
|
||||
public startApplying(opts: StartApplyingOpts): [URI, Promise<void>] | null {
|
||||
let res: [DiffZone, Promise<void>] | undefined = undefined
|
||||
|
||||
if (opts.from === 'QuickEdit') {
|
||||
res = await this._initializeWriteoverStream(opts) // rewrite
|
||||
res = this._initializeWriteoverStream(opts) // rewrite
|
||||
}
|
||||
else if (opts.from === 'ClickApply') {
|
||||
if (this._settingsService.state.globalSettings.enableFastApply) {
|
||||
res = await this._initializeSearchAndReplaceStream(opts) // fast apply
|
||||
const numCharsInFile = this._fileLengthOfGivenURI(opts.uri)
|
||||
if (numCharsInFile === null) return null
|
||||
if (numCharsInFile < 1000) { // slow apply for short files (especially important for empty files)
|
||||
res = this._initializeWriteoverStream(opts)
|
||||
}
|
||||
else {
|
||||
res = this._initializeSearchAndReplaceStream(opts) // fast apply
|
||||
}
|
||||
}
|
||||
else {
|
||||
res = await this._initializeWriteoverStream(opts) // rewrite
|
||||
res = this._initializeWriteoverStream(opts) // rewrite
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1301,19 +1280,21 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
}
|
||||
|
||||
|
||||
private async _initializeWriteoverStream(opts: StartApplyingOpts): Promise<[DiffZone, Promise<void>] | undefined> {
|
||||
private _initializeWriteoverStream(opts: StartApplyingOpts): [DiffZone, Promise<void>] | undefined {
|
||||
|
||||
const { from, } = opts
|
||||
const featureName: FeatureName = opts.from === 'ClickApply' ? 'Apply' : 'Ctrl+K'
|
||||
const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName]
|
||||
const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName] : undefined
|
||||
|
||||
|
||||
const uri = this._getURIBeforeStartApplying(opts)
|
||||
if (!uri) return
|
||||
|
||||
let uri: URI
|
||||
let startRange: 'fullFile' | [number, number]
|
||||
|
||||
let ctrlKZoneIfQuickEdit: CtrlKZone | null = null
|
||||
|
||||
if (from === 'ClickApply') {
|
||||
const uri_ = this._getActiveEditorURI()
|
||||
if (!uri_) return
|
||||
uri = uri_
|
||||
startRange = 'fullFile'
|
||||
}
|
||||
else if (from === 'QuickEdit') {
|
||||
@@ -1321,15 +1302,13 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
const ctrlKZone = this.diffAreaOfId[diffareaid]
|
||||
if (ctrlKZone?.type !== 'CtrlKZone') return
|
||||
ctrlKZoneIfQuickEdit = ctrlKZone
|
||||
const { startLine: startLine_, endLine: endLine_, _URI } = ctrlKZone
|
||||
uri = _URI
|
||||
const { startLine: startLine_, endLine: endLine_ } = ctrlKZone
|
||||
startRange = [startLine_, endLine_]
|
||||
}
|
||||
else {
|
||||
throw new Error(`Void: diff.type not recognized on: ${from}`)
|
||||
}
|
||||
|
||||
await this._voidModelService.initializeModel(uri)
|
||||
const { model } = this._voidModelService.getModel(uri)
|
||||
if (!model) return
|
||||
|
||||
@@ -1341,12 +1320,16 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
const originalCode = startRange === 'fullFile' ? originalFileCode : originalFileCode.split('\n').slice((startRange[0] - 1), (startRange[1] - 1) + 1).join('\n')
|
||||
const language = model.getLanguageId()
|
||||
let messages: LLMChatMessage[]
|
||||
let separateSystemMessage: string | undefined
|
||||
if (from === 'ClickApply') {
|
||||
const userContent = rewriteCode_userMessage({ originalCode, applyStr: opts.applyStr, language })
|
||||
messages = [
|
||||
{ role: 'system', content: rewriteCode_systemMessage, },
|
||||
{ role: 'user', content: userContent, }
|
||||
]
|
||||
const { messages: a, separateSystemMessage: b } = this._convertToLLMMessageService.prepareLLMSimpleMessages({
|
||||
systemMessage: rewriteCode_systemMessage,
|
||||
simpleMessages: [{ role: 'user', content: rewriteCode_userMessage({ originalCode, applyStr: opts.applyStr, language }), }],
|
||||
featureName,
|
||||
modelSelection,
|
||||
})
|
||||
messages = a
|
||||
separateSystemMessage = b
|
||||
}
|
||||
else if (from === 'QuickEdit') {
|
||||
if (!ctrlKZoneIfQuickEdit) return
|
||||
@@ -1356,12 +1339,17 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
const startLine = startRange === 'fullFile' ? 1 : startRange[0]
|
||||
const endLine = startRange === 'fullFile' ? model.getLineCount() : startRange[1]
|
||||
const { prefix, suffix } = voidPrefixAndSuffix({ fullFileStr: originalFileCode, startLine, endLine })
|
||||
const userContent = ctrlKStream_userMessage({ selection: originalCode, instructions: instructions, prefix, suffix, isOllamaFIM: false, fimTags: quickEditFIMTags, language })
|
||||
// type: 'messages',
|
||||
messages = [
|
||||
{ role: 'system', content: ctrlKStream_systemMessage({ quickEditFIMTags: quickEditFIMTags }), },
|
||||
{ role: 'user', content: userContent, }
|
||||
]
|
||||
const userContent = ctrlKStream_userMessage({ selection: originalCode, instructions: instructions, prefix, suffix, fimTags: quickEditFIMTags, language })
|
||||
|
||||
const { messages: a, separateSystemMessage: b } = this._convertToLLMMessageService.prepareLLMSimpleMessages({
|
||||
systemMessage: ctrlKStream_systemMessage({ quickEditFIMTags: quickEditFIMTags }),
|
||||
simpleMessages: [{ role: 'user', content: userContent, }],
|
||||
featureName,
|
||||
modelSelection,
|
||||
})
|
||||
messages = a
|
||||
separateSystemMessage = b
|
||||
|
||||
}
|
||||
else { throw new Error(`featureName ${from} is invalid`) }
|
||||
|
||||
@@ -1404,7 +1392,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
|
||||
// throws
|
||||
const onError = (e: { message: string; fullError: Error | null; }) => {
|
||||
this._notifyError(e)
|
||||
// this._notifyError(e)
|
||||
onDone()
|
||||
this._undoHistory(uri)
|
||||
throw e.fullError
|
||||
@@ -1425,10 +1413,6 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
|
||||
const latestStreamLocationMutable: StreamLocationMutable = { line: diffZone.startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 }
|
||||
|
||||
const featureName: FeatureName = opts.from === 'ClickApply' ? 'Apply' : 'Ctrl+K'
|
||||
const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName]
|
||||
const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[modelSelection.providerName]?.[modelSelection.modelName] : undefined
|
||||
|
||||
// allowed to throw errors - this is called inside a promise that handles everything
|
||||
const runWriteover = async () => {
|
||||
let shouldSendAnotherMessage = true
|
||||
@@ -1451,6 +1435,8 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
messages,
|
||||
modelSelection,
|
||||
modelSelectionOptions,
|
||||
separateSystemMessage,
|
||||
chatMode: null, // not chat
|
||||
onText: (params) => {
|
||||
const { fullText: fullText_ } = params
|
||||
const newText_ = fullText_.substring(fullTextSoFar.length, Infinity)
|
||||
@@ -1505,21 +1491,33 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
|
||||
|
||||
|
||||
|
||||
private async _initializeSearchAndReplaceStream(opts: StartApplyingOpts & { from: 'ClickApply' }): Promise<[DiffZone, Promise<void>] | undefined> {
|
||||
const { from, applyStr, uri: givenURI, } = opts
|
||||
let uri: URI
|
||||
|
||||
_uriOfGivenURI(givenURI: URI | 'current') {
|
||||
if (givenURI === 'current') {
|
||||
const uri_ = this._getActiveEditorURI()
|
||||
if (!uri_) return
|
||||
uri = uri_
|
||||
}
|
||||
else {
|
||||
uri = givenURI
|
||||
return uri_
|
||||
}
|
||||
return givenURI
|
||||
}
|
||||
_fileLengthOfGivenURI(givenURI: URI | 'current') {
|
||||
const uri = this._uriOfGivenURI(givenURI)
|
||||
if (!uri) return null
|
||||
const { model } = this._voidModelService.getModel(uri)
|
||||
if (!model) return null
|
||||
const numCharsInFile = model.getValueLength(EndOfLinePreference.LF)
|
||||
return numCharsInFile
|
||||
}
|
||||
|
||||
|
||||
private _initializeSearchAndReplaceStream(opts: StartApplyingOpts & { from: 'ClickApply' }): [DiffZone, Promise<void>] | undefined {
|
||||
const { from, applyStr, } = opts
|
||||
const featureName: FeatureName = 'Apply'
|
||||
const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName]
|
||||
const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName] : undefined
|
||||
|
||||
const uri = this._getURIBeforeStartApplying(opts)
|
||||
if (!uri) return
|
||||
|
||||
await this._voidModelService.initializeModel(uri)
|
||||
const { model } = this._voidModelService.getModel(uri)
|
||||
if (!model) return
|
||||
|
||||
@@ -1529,10 +1527,13 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
// build messages - ask LLM to generate search/replace block text
|
||||
const originalFileCode = model.getValue(EndOfLinePreference.LF)
|
||||
const userMessageContent = searchReplace_userMessage({ originalCode: originalFileCode, applyStr: applyStr })
|
||||
const messages: LLMChatMessage[] = [
|
||||
{ role: 'system', content: searchReplace_systemMessage },
|
||||
{ role: 'user', content: userMessageContent },
|
||||
]
|
||||
|
||||
const { messages, separateSystemMessage: separateSystemMessage } = this._convertToLLMMessageService.prepareLLMSimpleMessages({
|
||||
systemMessage: searchReplace_systemMessage,
|
||||
simpleMessages: [{ role: 'user', content: userMessageContent, }],
|
||||
featureName,
|
||||
modelSelection,
|
||||
})
|
||||
|
||||
// if URI is already streaming, return (should never happen, caller is responsible for checking)
|
||||
if (this._uriIsStreaming(uri)) return
|
||||
@@ -1576,20 +1577,22 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
}
|
||||
|
||||
|
||||
const errContentOfInvalidStr = (str: string & ReturnType<typeof findTextInCode>, blockOrig: string, blockNum: number, blocks: ExtractedSearchReplaceBlock[]) => {
|
||||
const errContentOfInvalidStr = (str: 'Not found' | 'Not unique' | 'Has overlap', blockOrig: string) => {
|
||||
|
||||
const descStr = str === `Not found` ?
|
||||
`The most recent ORIGINAL code could not be found in the file, so you were interrupted. The text in ORIGINAL must EXACTLY match lines of code. The problematic ORIGINAL code was:\n${JSON.stringify(blockOrig)}`
|
||||
: str === `Not unique` ?
|
||||
`The most recent ORIGINAL code shows up multiple times in the file, so you were interrupted. You might want to expand the ORIGINAL excerpt so it's unique. The problematic ORIGINAL code was:\n${JSON.stringify(blockOrig)}`
|
||||
: ``
|
||||
: str === 'Has overlap' ?
|
||||
`The most recent ORIGINAL code has overlap with another ORIGINAL code block that you outputted. Do NOT output any overlapping edits. The problematic ORIGINAL code was:\n${JSON.stringify(blockOrig)}`
|
||||
: ``
|
||||
|
||||
// string of <<<<< ORIGINAL >>>>> REPLACE blocks so far so LLM can understand what it currently has
|
||||
// const blocksSoFarStr = blocks.slice(0, blockNum).map(block => `${ORIGINAL}\n${block.orig}\n${DIVIDER}\n${block.final}\n${FINAL}`).join('\n')
|
||||
// const soFarStr = blocksSoFarStr ? `These are the Search/Replace blocks that have been applied so far:${tripleTick[0]}\n${blocksSoFarStr}\n${tripleTick[1]}` : ''
|
||||
// const continueMsg = soFarStr ? `${soFarStr}Please continue outputting SEARCH/REPLACE blocks starting where this leaves off.` : ''
|
||||
// const errMsg = `${descStr}${continueMsg ? `\n${continueMsg}` : ''}`
|
||||
const soFarStr = 'All of your previous outputs have been ignored. Please re-output ALL SEARCH/REPLACE blocks starting from the first one, and avoid the error.'
|
||||
const soFarStr = 'All of your previous outputs have been ignored. Please re-output ALL SEARCH/REPLACE blocks starting from the first one, and avoid the error this time.'
|
||||
const errMsg = `${descStr}\n${soFarStr}`
|
||||
return errMsg
|
||||
|
||||
@@ -1608,10 +1611,10 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
}
|
||||
|
||||
const onError = (e: { message: string; fullError: Error | null; }) => {
|
||||
this._notifyError(e)
|
||||
// this._notifyError(e)
|
||||
onDone()
|
||||
this._undoHistory(uri)
|
||||
throw e.fullError // throw error h
|
||||
throw e.fullError || new Error(e.message) // throw error h
|
||||
}
|
||||
|
||||
// refresh now in case onText takes a while to get 1st message
|
||||
@@ -1624,11 +1627,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
const addedTrackingZoneOfBlockNum: TrackingZone<SearchReplaceDiffAreaMetadata>[] = []
|
||||
diffZone._streamState.line = 1
|
||||
|
||||
const featureName: FeatureName = 'Apply'
|
||||
const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName]
|
||||
const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[modelSelection.providerName]?.[modelSelection.modelName] : undefined
|
||||
|
||||
const N_RETRIES = 5
|
||||
const N_RETRIES = 4
|
||||
|
||||
// allowed to throw errors - this is called inside a promise that handles everything
|
||||
const runSearchReplace = async () => {
|
||||
@@ -1659,6 +1658,8 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
messages,
|
||||
modelSelection,
|
||||
modelSelectionOptions,
|
||||
separateSystemMessage,
|
||||
chatMode: null, // not chat
|
||||
onText: (params) => {
|
||||
const { fullText } = params
|
||||
// blocks are [done done done ... {writingFinal|writingOriginal}]
|
||||
@@ -1674,7 +1675,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
// update stream state to the first line of original if some portion of original has been written
|
||||
if (shouldUpdateOrigStreamStyle && block.orig.trim().length >= 20) {
|
||||
const startingAtLine = diffZone._streamState.line ?? 1 // dont go backwards if already have a stream line
|
||||
const originalRange = findTextInCode(block.orig, originalFileCode, startingAtLine)
|
||||
const originalRange = findTextInCode(block.orig, originalFileCode, false, startingAtLine)
|
||||
if (typeof originalRange !== 'string') {
|
||||
const [startLine, _] = convertOriginalRangeToFinalRange(originalRange)
|
||||
diffZone._streamState.line = startLine
|
||||
@@ -1700,19 +1701,27 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
// if this is the first time we're seeing this block, add it as a diffarea so we can start streaming in it
|
||||
if (!(blockNum in addedTrackingZoneOfBlockNum)) {
|
||||
|
||||
|
||||
const originalBounds = findTextInCode(block.orig, originalFileCode)
|
||||
const originalBounds = findTextInCode(block.orig, originalFileCode, true)
|
||||
// if error
|
||||
if (typeof originalBounds === 'string') {
|
||||
// Check for overlap with existing modified ranges
|
||||
const hasOverlap = addedTrackingZoneOfBlockNum.some(trackingZone => {
|
||||
const [existingStart, existingEnd] = trackingZone.metadata.originalBounds;
|
||||
const hasNoOverlap = endLine < existingStart || startLine > existingEnd
|
||||
return !hasNoOverlap
|
||||
});
|
||||
|
||||
if (typeof originalBounds === 'string' || hasOverlap) {
|
||||
const errorMessage = typeof originalBounds === 'string' ? originalBounds : 'Has overlap' as const
|
||||
|
||||
console.log('--------------Error finding text in code:')
|
||||
console.log('originalFileCode', { originalFileCode })
|
||||
console.log('fullText', { fullText })
|
||||
console.log('error:', originalBounds)
|
||||
console.log('error:', errorMessage)
|
||||
console.log('block.orig:', block.orig)
|
||||
console.log('---------')
|
||||
const content = errContentOfInvalidStr(originalBounds, block.orig, blockNum, blocks)
|
||||
const content = errContentOfInvalidStr(errorMessage, block.orig)
|
||||
messages.push(
|
||||
{ role: 'assistant', content: fullText, anthropicReasoning: null }, // latest output
|
||||
{ role: 'assistant', content: fullText }, // latest output
|
||||
{ role: 'user', content: content } // user explanation of what's wrong
|
||||
)
|
||||
|
||||
@@ -1740,14 +1749,15 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
return
|
||||
}
|
||||
|
||||
console.log('---------adding-------')
|
||||
console.log('CURRENT TEXT!!!', { current: model?.getValue() })
|
||||
console.log('block', deepClone(block))
|
||||
console.log('origBounds', originalBounds)
|
||||
|
||||
|
||||
const [startLine, endLine] = convertOriginalRangeToFinalRange(originalBounds)
|
||||
console.log('start end', startLine, endLine)
|
||||
|
||||
// console.log('---------adding-------')
|
||||
// console.log('CURRENT TEXT!!!', { current: model?.getValue() })
|
||||
// console.log('block', deepClone(block))
|
||||
// console.log('origBounds', originalBounds)
|
||||
// console.log('start end', startLine, endLine)
|
||||
|
||||
// otherwise if no error, add the position as a diffarea
|
||||
const adding: Omit<TrackingZone<SearchReplaceDiffAreaMetadata>, 'diffareaid'> = {
|
||||
@@ -1804,7 +1814,6 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
onFinalMessage: async (params) => {
|
||||
const { fullText } = params
|
||||
|
||||
console.log('DONE - editCode!', { fullText })
|
||||
|
||||
// 1. wait 500ms and fix lint errors - call lint error workflow
|
||||
// (update react state to say "Fixing errors")
|
||||
@@ -1819,10 +1828,11 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
// IMPORTANT - sort by lineNum
|
||||
addedTrackingZoneOfBlockNum.sort((a, b) => a.metadata.originalBounds[0] - b.metadata.originalBounds[0])
|
||||
|
||||
const { model } = this._voidModelService.getModel(uri)
|
||||
console.log('CURRENT TEXT!!!', { current: model?.getValue() })
|
||||
console.log('addedTrackingZoneOfBlockNum', addedTrackingZoneOfBlockNum)
|
||||
console.log('blocks', deepClone(blocks))
|
||||
// const { model } = this._voidModelService.getModel(uri)
|
||||
// console.log('DONE - editCode!', { fullText })
|
||||
// console.log('CURRENT TEXT!!!', { current: model?.getValue() })
|
||||
// console.log('addedTrackingZoneOfBlockNum', addedTrackingZoneOfBlockNum)
|
||||
// console.log('blocks', deepClone(blocks))
|
||||
|
||||
for (let blockNum = addedTrackingZoneOfBlockNum.length - 1; blockNum >= 0; blockNum -= 1) {
|
||||
const { originalBounds } = addedTrackingZoneOfBlockNum[blockNum].metadata
|
||||
@@ -1917,6 +1927,8 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
|
||||
|
||||
interruptURIStreaming({ uri }: { uri: URI }) {
|
||||
if (!this._uriIsStreaming(uri)) return
|
||||
this._undoHistory(uri)
|
||||
// brute force for now is OK
|
||||
for (const diffareaid of this.diffAreasOfURI[uri.fsPath] || []) {
|
||||
const diffArea = this.diffAreaOfId[diffareaid]
|
||||
@@ -1924,7 +1936,6 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
if (!diffArea._streamState.isStreaming) continue
|
||||
this._stopIfStreaming(diffArea)
|
||||
}
|
||||
this._undoHistory(uri)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -7,13 +7,20 @@ import { Event } from '../../../../base/common/event.js';
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { Diff, DiffArea } from './editCodeService.js';
|
||||
|
||||
import { Diff, DiffArea, VoidFileSnapshot } from '../common/editCodeServiceTypes.js';
|
||||
|
||||
|
||||
export type StartBehavior = 'accept-conflicts' | 'reject-conflicts' | 'keep-conflicts'
|
||||
|
||||
export type StartApplyingOpts = ({
|
||||
export type CallBeforeStartApplyingOpts = {
|
||||
from: 'QuickEdit';
|
||||
diffareaid: number; // id of the CtrlK area (contains text selection)
|
||||
} | {
|
||||
from: 'ClickApply';
|
||||
uri: 'current' | URI;
|
||||
}
|
||||
|
||||
export type StartApplyingOpts = {
|
||||
from: 'QuickEdit';
|
||||
diffareaid: number; // id of the CtrlK area (contains text selection)
|
||||
startBehavior: StartBehavior;
|
||||
@@ -22,9 +29,7 @@ export type StartApplyingOpts = ({
|
||||
applyStr: string;
|
||||
uri: 'current' | URI;
|
||||
startBehavior: StartBehavior;
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
|
||||
export type AddCtrlKOpts = {
|
||||
startLine: number,
|
||||
@@ -37,7 +42,8 @@ export const IEditCodeService = createDecorator<IEditCodeService>('editCodeServi
|
||||
export interface IEditCodeService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
startApplying(opts: StartApplyingOpts): Promise<[URI, Promise<void>] | null>;
|
||||
callBeforeStartApplying(opts: CallBeforeStartApplyingOpts): Promise<void>;
|
||||
startApplying(opts: StartApplyingOpts): [URI, Promise<void>] | null;
|
||||
addCtrlKZone(opts: AddCtrlKOpts): number | undefined;
|
||||
removeCtrlKZone(opts: { diffareaid: number }): void;
|
||||
|
||||
@@ -49,7 +55,7 @@ export interface IEditCodeService {
|
||||
|
||||
// events
|
||||
onDidAddOrDeleteDiffZones: Event<{ uri: URI }>;
|
||||
onDidChangeDiffsInDiffZone: Event<{ uri: URI; diffareaid: number }>; // only fires when not streaming!!! streaming would be too much
|
||||
onDidChangeDiffsInDiffZoneNotStreaming: Event<{ uri: URI; diffareaid: number }>; // only fires when not streaming!!! streaming would be too much
|
||||
onDidChangeStreamingInDiffZone: Event<{ uri: URI; diffareaid: number }>;
|
||||
onDidChangeStreamingInCtrlKZone: Event<{ uri: URI; diffareaid: number }>;
|
||||
|
||||
@@ -61,4 +67,6 @@ export interface IEditCodeService {
|
||||
interruptURIStreaming(opts: { uri: URI }): void;
|
||||
|
||||
// testDiffs(): void;
|
||||
getVoidFileSnapshot(uri: URI): VoidFileSnapshot;
|
||||
restoreVoidFileSnapshot(uri: URI, snapshot: VoidFileSnapshot): void;
|
||||
}
|
||||
|
||||
@@ -3,34 +3,9 @@
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ComputedDiff } from '../../common/editCodeServiceTypes.js';
|
||||
import { diffLines } from '../react/out/diff/index.js'
|
||||
|
||||
export type ComputedDiff = {
|
||||
type: 'edit';
|
||||
originalCode: string;
|
||||
originalStartLine: number;
|
||||
originalEndLine: number;
|
||||
code: string;
|
||||
startLine: number; // 1-indexed
|
||||
endLine: number;
|
||||
} | {
|
||||
type: 'insertion';
|
||||
// originalCode: string;
|
||||
originalStartLine: number; // insertion starts on column 0 of this
|
||||
// originalEndLine: number;
|
||||
code: string;
|
||||
startLine: number;
|
||||
endLine: number;
|
||||
} | {
|
||||
type: 'deletion';
|
||||
originalCode: string;
|
||||
originalStartLine: number;
|
||||
originalEndLine: number;
|
||||
// code: string;
|
||||
startLine: number; // deletion starts on column 0 of this
|
||||
// endLine: number;
|
||||
}
|
||||
|
||||
export function findDiffs(oldStr: string, newStr: string) {
|
||||
|
||||
// this makes it so the end of the file always ends with a \n (if you don't have this, then diffing E vs E\n gives an "edit". With it, you end up diffing E\n vs E\n\n which now properly gives an insertion)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-editor .void-sweepIdxBG {
|
||||
.monaco-editor .void-sweepIdxBG {
|
||||
background-color: var(--vscode-void-sweepIdxBG);
|
||||
}
|
||||
|
||||
@@ -23,10 +23,30 @@
|
||||
background-color: var(--vscode-void-redBG);
|
||||
}
|
||||
|
||||
.void-watermark-button {
|
||||
margin: 8px 0;
|
||||
|
||||
/* Renamed from void-watermark-button to void-openfolder-button */
|
||||
.void-openfolder-button {
|
||||
padding: 8px 20px;
|
||||
background-color: #3b82f6;
|
||||
background-color: #306dce;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
.void-openfolder-button:hover {
|
||||
background-color: #2563eb;
|
||||
}
|
||||
.void-openfolder-button:active {
|
||||
background-color: #2563eb;
|
||||
}
|
||||
|
||||
/* Added for Open SSH button with slightly darker color */
|
||||
.void-openssh-button {
|
||||
padding: 8px 20px;
|
||||
background-color: #656565; /* Slightly darker than the #5a5a5a in the TS file */
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
@@ -35,134 +55,150 @@
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
.void-watermark-button:hover {
|
||||
background-color: #2563eb;
|
||||
.void-openssh-button:hover {
|
||||
background-color: #474747; /* Darker on hover */
|
||||
}
|
||||
.void-watermark-button:active {
|
||||
background-color: #2563eb;
|
||||
.void-openssh-button:active {
|
||||
background-color: #474747;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
.void-settings-watermark-button {
|
||||
margin: 8px 0;
|
||||
padding: 8px 20px;
|
||||
background-color: var(--vscode-input-background);
|
||||
margin: 8px 0;
|
||||
padding: 8px 20px;
|
||||
background-color: var(--vscode-input-background);
|
||||
color: var(--vscode-input-foreground);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
outline: none !important;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.void-settings-watermark-button:hover {
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
|
||||
.void-settings-watermark-button:active {
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
.void-link {
|
||||
color: #3b82f6;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.void-link:hover {
|
||||
opacity: 80%;
|
||||
}
|
||||
|
||||
/* styles for all containers used by void */
|
||||
.void-scope {
|
||||
--scrollbar-vertical-width: 8px;
|
||||
--scrollbar-horizontal-height: 6px;
|
||||
}
|
||||
|
||||
/* Target both void-scope and all its descendants with scrollbars */
|
||||
.void-scope,
|
||||
.void-scope * {
|
||||
scrollbar-width: thin !important;
|
||||
scrollbar-color: var(--void-bg-1) var(--void-bg-3) !important;
|
||||
/* For Firefox */
|
||||
}
|
||||
|
||||
.void-scope::-webkit-scrollbar,
|
||||
.void-scope *::-webkit-scrollbar {
|
||||
width: var(--scrollbar-vertical-width) !important;
|
||||
height: var(--scrollbar-horizontal-height) !important;
|
||||
background-color: var(--void-bg-3) !important;
|
||||
}
|
||||
|
||||
.void-scope::-webkit-scrollbar-thumb,
|
||||
.void-scope *::-webkit-scrollbar-thumb {
|
||||
background-color: var(--void-bg-1) !important;
|
||||
border-radius: 4px !important;
|
||||
border: none !important;
|
||||
-webkit-box-shadow: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.void-scope::-webkit-scrollbar-thumb:hover,
|
||||
.void-scope *::-webkit-scrollbar-thumb:hover {
|
||||
background-color: var(--void-bg-1) !important;
|
||||
filter: brightness(1.1) !important;
|
||||
}
|
||||
|
||||
.void-scope::-webkit-scrollbar-thumb:active,
|
||||
.void-scope *::-webkit-scrollbar-thumb:active {
|
||||
background-color: var(--void-bg-1) !important;
|
||||
filter: brightness(1.2) !important;
|
||||
}
|
||||
|
||||
.void-scope::-webkit-scrollbar-track,
|
||||
.void-scope *::-webkit-scrollbar-track {
|
||||
background-color: var(--void-bg-3) !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.void-scope::-webkit-scrollbar-corner,
|
||||
.void-scope *::-webkit-scrollbar-corner {
|
||||
background-color: var(--void-bg-3) !important;
|
||||
}
|
||||
|
||||
/* Add void-scrollable-element styles to match */
|
||||
.void-scrollable-element {
|
||||
background-color: var(--vscode-editor-background);
|
||||
--scrollbar-vertical-width: 14px;
|
||||
--scrollbar-horizontal-height: 6px;
|
||||
overflow: auto;
|
||||
/* Ensure scrollbars are shown when needed */
|
||||
}
|
||||
|
||||
.void-scrollable-element,
|
||||
.void-scrollable-element * {
|
||||
scrollbar-width: thin !important;
|
||||
/* For Firefox */
|
||||
scrollbar-color: var(--void-bg-1) var(--void-bg-3) !important;
|
||||
/* For Firefox */
|
||||
}
|
||||
|
||||
.void-scrollable-element::-webkit-scrollbar,
|
||||
.void-scrollable-element *::-webkit-scrollbar {
|
||||
width: 14px !important;
|
||||
height: 4px !important;
|
||||
}
|
||||
|
||||
.void-scrollable-element::-webkit-scrollbar-track,
|
||||
.void-scrollable-element *::-webkit-scrollbar-track {
|
||||
background: transparent !important;
|
||||
width: var(--scrollbar-vertical-width) !important;
|
||||
height: var(--scrollbar-horizontal-height) !important;
|
||||
background-color: var(--void-bg-3) !important;
|
||||
}
|
||||
|
||||
.void-scrollable-element::-webkit-scrollbar-thumb,
|
||||
.void-scrollable-element *::-webkit-scrollbar-thumb {
|
||||
background-color: transparent !important;
|
||||
border-radius: 0px !important;
|
||||
background-color: var(--void-bg-1) !important;
|
||||
border-radius: 4px !important;
|
||||
border: none !important;
|
||||
-webkit-box-shadow: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.void-scrollable-element::-webkit-scrollbar-thumb:hover,
|
||||
.void-scrollable-element *::-webkit-scrollbar-thumb:hover {
|
||||
background-color: var(--vscode-scrollbarSlider-hoverBackground) !important;
|
||||
background-color: var(--void-bg-1) !important;
|
||||
filter: brightness(1.1) !important;
|
||||
}
|
||||
|
||||
.void-scrollable-element::-webkit-scrollbar-thumb:active,
|
||||
.void-scrollable-element *::-webkit-scrollbar-thumb:active {
|
||||
background-color: var(--vscode-scrollbarSlider-activeBackground) !important;
|
||||
background-color: var(--void-bg-1) !important;
|
||||
filter: brightness(1.2) !important;
|
||||
}
|
||||
|
||||
.void-scrollable-element::-webkit-scrollbar-track,
|
||||
.void-scrollable-element *::-webkit-scrollbar-track {
|
||||
background-color: var(--void-bg-3) !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.void-scrollable-element::-webkit-scrollbar-corner,
|
||||
.void-scrollable-element *::-webkit-scrollbar-corner {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.void-scrollable-element.show-scrollbar-0::-webkit-scrollbar-thumb,
|
||||
.void-scrollable-element.show-scrollbar-0 *::-webkit-scrollbar-thumb {
|
||||
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 0%, transparent) !important;
|
||||
}
|
||||
|
||||
.void-scrollable-element.show-scrollbar-1::-webkit-scrollbar-thumb,
|
||||
.void-scrollable-element.show-scrollbar-1 *::-webkit-scrollbar-thumb {
|
||||
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 10%, transparent) !important;
|
||||
}
|
||||
|
||||
.void-scrollable-element.show-scrollbar-2::-webkit-scrollbar-thumb,
|
||||
.void-scrollable-element.show-scrollbar-2 *::-webkit-scrollbar-thumb {
|
||||
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 20%, transparent) !important;
|
||||
}
|
||||
|
||||
.void-scrollable-element.show-scrollbar-3::-webkit-scrollbar-thumb,
|
||||
.void-scrollable-element.show-scrollbar-3 *::-webkit-scrollbar-thumb {
|
||||
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 30%, transparent) !important;
|
||||
}
|
||||
|
||||
.void-scrollable-element.show-scrollbar-4::-webkit-scrollbar-thumb,
|
||||
.void-scrollable-element.show-scrollbar-4 *::-webkit-scrollbar-thumb {
|
||||
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 40%, transparent) !important;
|
||||
}
|
||||
|
||||
.void-scrollable-element.show-scrollbar-5::-webkit-scrollbar-thumb,
|
||||
.void-scrollable-element.show-scrollbar-5 *::-webkit-scrollbar-thumb {
|
||||
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 50%, transparent) !important;
|
||||
}
|
||||
|
||||
.void-scrollable-element.show-scrollbar-6::-webkit-scrollbar-thumb,
|
||||
.void-scrollable-element.show-scrollbar-6 *::-webkit-scrollbar-thumb {
|
||||
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 60%, transparent) !important;
|
||||
}
|
||||
|
||||
.void-scrollable-element.show-scrollbar-7::-webkit-scrollbar-thumb,
|
||||
.void-scrollable-element.show-scrollbar-7 *::-webkit-scrollbar-thumb {
|
||||
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 70%, transparent) !important;
|
||||
}
|
||||
|
||||
.void-scrollable-element.show-scrollbar-8::-webkit-scrollbar-thumb,
|
||||
.void-scrollable-element.show-scrollbar-8 *::-webkit-scrollbar-thumb {
|
||||
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 80%, transparent) !important;
|
||||
}
|
||||
|
||||
.void-scrollable-element.show-scrollbar-9::-webkit-scrollbar-thumb,
|
||||
.void-scrollable-element.show-scrollbar-9 *::-webkit-scrollbar-thumb {
|
||||
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 90%, transparent) !important;
|
||||
}
|
||||
|
||||
.void-scrollable-element.show-scrollbar-10::-webkit-scrollbar-thumb,
|
||||
.void-scrollable-element.show-scrollbar-10 *::-webkit-scrollbar-thumb {
|
||||
background-color: var(--vscode-scrollbarSlider-background) !important;
|
||||
background-color: var(--void-bg-3) !important;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import * as dom from '../../../../base/browser/dom.js';
|
||||
import { IMetricsService } from '../common/metricsService.js';
|
||||
|
||||
|
||||
|
||||
export interface IMetricsPollService {
|
||||
readonly _serviceBrand: undefined;
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ function saveStylesFile() {
|
||||
} catch (err) {
|
||||
console.error('[scope-tailwind] Error saving styles.css:', err);
|
||||
}
|
||||
}, 4000);
|
||||
}, 6000);
|
||||
}
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
+183
-138
@@ -1,12 +1,17 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { useAccessor, useCommandBarState, useCommandBarURIListener, useSettingsState } from '../util/services.js'
|
||||
import { usePromise, useRefState } from '../util/helpers.js'
|
||||
import { isFeatureNameDisabled } from '../../../../common/voidSettingsTypes.js'
|
||||
import { URI } from '../../../../../../../base/common/uri.js'
|
||||
import { FileSymlink, LucideIcon, RotateCw } from 'lucide-react'
|
||||
import { FileSymlink, LucideIcon, RotateCw, Terminal } from 'lucide-react'
|
||||
import { Check, X, Square, Copy, Play, } from 'lucide-react'
|
||||
import { getBasename, ListableToolItem, ToolChildrenWrapper } from '../sidebar-tsx/SidebarChat.js'
|
||||
import { ChatMarkdownRender } from './ChatMarkdownRender.js'
|
||||
import { PlacesType, VariantType } from 'react-tooltip'
|
||||
|
||||
enum CopyButtonText {
|
||||
Idle = 'Copy',
|
||||
@@ -16,30 +21,28 @@ enum CopyButtonText {
|
||||
|
||||
|
||||
type IconButtonProps = {
|
||||
onClick: () => void;
|
||||
Icon: LucideIcon
|
||||
disabled?: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const IconShell1 = ({ onClick, Icon, disabled, className }: IconButtonProps) => (
|
||||
export const IconShell1 = ({ onClick, Icon, disabled, className, ...props }: IconButtonProps & React.ButtonHTMLAttributes<HTMLButtonElement>) => (
|
||||
<button
|
||||
disabled={disabled}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onClick?.();
|
||||
onClick?.(e);
|
||||
}}
|
||||
// border border-void-border-1 rounded
|
||||
className={`
|
||||
size-[22px]
|
||||
p-[4px]
|
||||
size-[18px]
|
||||
p-[2px]
|
||||
flex items-center justify-center
|
||||
text-sm bg-void-bg-3 text-void-fg-1
|
||||
text-sm text-void-fg-3
|
||||
hover:brightness-110
|
||||
border border-void-border-1 rounded
|
||||
disabled:opacity-50 disabled:cursor-not-allowed
|
||||
${className}
|
||||
`}
|
||||
{...props}
|
||||
>
|
||||
<Icon />
|
||||
</button>
|
||||
@@ -64,9 +67,9 @@ export const IconShell1 = ({ onClick, Icon, disabled, className }: IconButtonPro
|
||||
// </button>
|
||||
// )
|
||||
|
||||
const COPY_FEEDBACK_TIMEOUT = 1000 // amount of time to say 'Copied!'
|
||||
const COPY_FEEDBACK_TIMEOUT = 1500 // amount of time to say 'Copied!'
|
||||
|
||||
const CopyButton = ({ codeStr }: { codeStr: string }) => {
|
||||
export const CopyButton = ({ codeStr, toolTipName }: { codeStr: string | (() => Promise<string> | string), toolTipName: string }) => {
|
||||
const accessor = useAccessor()
|
||||
|
||||
const metricsService = accessor.get('IMetricsService')
|
||||
@@ -80,8 +83,8 @@ const CopyButton = ({ codeStr }: { codeStr: string }) => {
|
||||
}, COPY_FEEDBACK_TIMEOUT)
|
||||
}, [copyButtonText])
|
||||
|
||||
const onCopy = useCallback(() => {
|
||||
clipboardService.writeText(codeStr)
|
||||
const onCopy = useCallback(async () => {
|
||||
clipboardService.writeText(typeof codeStr === 'string' ? codeStr : await codeStr())
|
||||
.then(() => { setCopyButtonText(CopyButtonText.Copied) })
|
||||
.catch(() => { setCopyButtonText(CopyButtonText.Error) })
|
||||
metricsService.capture('Copy Code', { length: codeStr.length }) // capture the length only
|
||||
@@ -90,18 +93,14 @@ const CopyButton = ({ codeStr }: { codeStr: string }) => {
|
||||
return <IconShell1
|
||||
Icon={copyButtonText === CopyButtonText.Copied ? Check : copyButtonText === CopyButtonText.Error ? X : Copy}
|
||||
onClick={onCopy}
|
||||
{...tooltipPropsForApplyBlock({ tooltipName: toolTipName })}
|
||||
/>
|
||||
}
|
||||
|
||||
|
||||
// state persisted for duration of react only
|
||||
// TODO change this to use type `ChatThreads.applyBoxState[applyBoxId]`
|
||||
const applyingURIOfApplyBoxIdRef: { current: { [applyBoxId: string]: URI | undefined } } = { current: {} }
|
||||
|
||||
|
||||
|
||||
|
||||
export const JumpToFileButton = ({ uri }: { uri: URI | 'current' }) => {
|
||||
export const JumpToFileButton = ({ uri, ...props }: { uri: URI | 'current' } & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
|
||||
const accessor = useAccessor()
|
||||
const commandService = accessor.get('ICommandService')
|
||||
|
||||
@@ -111,185 +110,229 @@ export const JumpToFileButton = ({ uri }: { uri: URI | 'current' }) => {
|
||||
onClick={() => {
|
||||
commandService.executeCommand('vscode.open', uri, { preview: true })
|
||||
}}
|
||||
{...tooltipPropsForApplyBlock({ tooltipName: 'Go to file' })}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
return jumpToFileButton
|
||||
}
|
||||
|
||||
export const useApplyButtonHTML = ({ codeStr, applyBoxId, uri }: { codeStr: string, applyBoxId: string, uri: URI | 'current' }) => {
|
||||
|
||||
|
||||
export const JumpToTerminalButton = ({ onClick }: { onClick: () => void }) => {
|
||||
return (
|
||||
<IconShell1
|
||||
Icon={Terminal}
|
||||
onClick={onClick}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// state persisted for duration of react only
|
||||
// TODO change this to use type `ChatThreads.applyBoxState[applyBoxId]`
|
||||
const applyingURIOfApplyBoxIdRef: { current: { [applyBoxId: string]: URI | undefined } } = { current: {} }
|
||||
|
||||
const getUriBeingApplied = (applyBoxId: string) => {
|
||||
return applyingURIOfApplyBoxIdRef.current[applyBoxId] ?? null
|
||||
}
|
||||
|
||||
|
||||
export const useApplyButtonState = ({ applyBoxId, uri }: { applyBoxId: string, uri: URI | 'current' }) => {
|
||||
|
||||
const settingsState = useSettingsState()
|
||||
const isDisabled = !!isFeatureNameDisabled('Apply', settingsState) || !applyBoxId
|
||||
|
||||
const accessor = useAccessor()
|
||||
const editCodeService = accessor.get('IEditCodeService')
|
||||
const voidCommandBarService = accessor.get('IVoidCommandBarService')
|
||||
const metricsService = accessor.get('IMetricsService')
|
||||
|
||||
const [_, rerender] = useState(0)
|
||||
|
||||
const getUriBeingApplied = useCallback(() => {
|
||||
return applyingURIOfApplyBoxIdRef.current[applyBoxId] ?? null
|
||||
}, [applyBoxId])
|
||||
|
||||
const getStreamState = useCallback(() => {
|
||||
const uri = getUriBeingApplied()
|
||||
const uri = getUriBeingApplied(applyBoxId)
|
||||
if (!uri) return 'idle-no-changes'
|
||||
return voidCommandBarService.getStreamState(uri)
|
||||
}, [voidCommandBarService, getUriBeingApplied])
|
||||
}, [voidCommandBarService, applyBoxId])
|
||||
|
||||
// listen for stream updates on this box
|
||||
|
||||
|
||||
useCommandBarURIListener(useCallback((uri_) => {
|
||||
const shouldUpdate = (
|
||||
getUriBeingApplied()?.fsPath === uri_.fsPath
|
||||
getUriBeingApplied(applyBoxId)?.fsPath === uri_.fsPath
|
||||
|| (uri !== 'current' && uri.fsPath === uri_.fsPath)
|
||||
)
|
||||
if (!shouldUpdate) return
|
||||
rerender(c => c + 1)
|
||||
}, [applyBoxId, editCodeService, getUriBeingApplied, uri])
|
||||
if (shouldUpdate) {
|
||||
rerender(c => c + 1)
|
||||
}
|
||||
}, [applyBoxId, uri]))
|
||||
|
||||
const currStreamState = getStreamState()
|
||||
|
||||
|
||||
return {
|
||||
getStreamState,
|
||||
isDisabled,
|
||||
currStreamState,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
type IndicatorColor = 'green' | 'orange' | 'dark' | 'yellow' | null
|
||||
export const StatusIndicator = ({ indicatorColor, title, className, ...props }: { indicatorColor: IndicatorColor, title?: React.ReactNode, className?: string } & React.HTMLAttributes<HTMLDivElement>) => {
|
||||
return (
|
||||
<div className={`flex flex-row text-void-fg-3 text-xs items-center gap-1.5 ${className}`} {...props}>
|
||||
{title && <span className='opacity-80'>{title}</span>}
|
||||
<div
|
||||
className={` size-1.5 rounded-full border
|
||||
${indicatorColor === 'dark' ? 'bg-void-bg-3 border-void-border-1' :
|
||||
indicatorColor === 'orange' ? 'bg-orange-500 border-orange-500 shadow-[0_0_4px_0px_rgba(234,88,12,0.6)]' :
|
||||
indicatorColor === 'green' ? 'bg-green-500 border-green-500 shadow-[0_0_4px_0px_rgba(22,163,74,0.6)]' :
|
||||
indicatorColor === 'yellow' ? 'bg-yellow-500 border-yellow-500 shadow-[0_0_4px_0px_rgba(22,163,74,0.6)]' :
|
||||
'bg-void-border-1 border-void-border-1'
|
||||
}
|
||||
`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const tooltipPropsForApplyBlock = ({ tooltipName, color = undefined, position = 'top', offset = undefined }: { tooltipName: string, color?: IndicatorColor, position?: PlacesType, offset?: number }) => ({
|
||||
'data-tooltip-id': color === 'orange' ? `void-tooltip-orange` : color === 'green' ? 'void-tooltip-green' : 'void-tooltip',
|
||||
'data-tooltip-place': position as PlacesType,
|
||||
'data-tooltip-content': `${tooltipName}`,
|
||||
'data-tooltip-offset': offset,
|
||||
})
|
||||
|
||||
|
||||
export const StatusIndicatorForApplyButton = ({ applyBoxId, uri }: { applyBoxId: string, uri: URI | 'current' } & React.HTMLAttributes<HTMLDivElement>) => {
|
||||
|
||||
const { currStreamState } = useApplyButtonState({ applyBoxId, uri })
|
||||
|
||||
const color = (
|
||||
currStreamState === 'idle-no-changes' ? 'dark' :
|
||||
currStreamState === 'streaming' ? 'orange' :
|
||||
currStreamState === 'idle-has-changes' ? 'green' :
|
||||
null
|
||||
)
|
||||
|
||||
const tooltipName = (
|
||||
currStreamState === 'idle-no-changes' ? 'Done' :
|
||||
currStreamState === 'streaming' ? 'Applying' :
|
||||
currStreamState === 'idle-has-changes' ? 'Done' : // also 'Done'? 'Applied' looked bad
|
||||
''
|
||||
)
|
||||
|
||||
const statusIndicatorHTML = <StatusIndicator
|
||||
key={currStreamState}
|
||||
className='mx-2'
|
||||
indicatorColor={color}
|
||||
{...tooltipPropsForApplyBlock({ tooltipName, color, position: 'top', offset: 12 })}
|
||||
/>
|
||||
return statusIndicatorHTML
|
||||
}
|
||||
|
||||
|
||||
export const ApplyButtonsHTML = ({ codeStr, applyBoxId, reapplyIcon, uri }: { codeStr: string, applyBoxId: string, reapplyIcon: boolean, uri: URI | 'current' }) => {
|
||||
const accessor = useAccessor()
|
||||
const editCodeService = accessor.get('IEditCodeService')
|
||||
const metricsService = accessor.get('IMetricsService')
|
||||
|
||||
const {
|
||||
currStreamState,
|
||||
isDisabled,
|
||||
getStreamState,
|
||||
} = useApplyButtonState({ applyBoxId, uri })
|
||||
|
||||
const onClickSubmit = useCallback(async () => {
|
||||
if (isDisabled) return
|
||||
if (getStreamState() === 'streaming') return
|
||||
const [newApplyingUri, applyDonePromise] = await editCodeService.startApplying({
|
||||
const opts = {
|
||||
from: 'ClickApply',
|
||||
applyStr: codeStr,
|
||||
uri: uri,
|
||||
startBehavior: 'keep-conflicts',
|
||||
}) ?? []
|
||||
startBehavior: 'reject-conflicts',
|
||||
} as const
|
||||
|
||||
await editCodeService.callBeforeStartApplying(opts)
|
||||
const [newApplyingUri, applyDonePromise] = editCodeService.startApplying(opts) ?? []
|
||||
|
||||
// catch any errors by interrupting the stream
|
||||
applyDonePromise?.catch(e => { if (newApplyingUri) editCodeService.interruptURIStreaming({ uri: newApplyingUri }) })
|
||||
applyDonePromise?.catch(e => {
|
||||
const uri = getUriBeingApplied(applyBoxId)
|
||||
if (uri) editCodeService.interruptURIStreaming({ uri: uri })
|
||||
})
|
||||
|
||||
applyingURIOfApplyBoxIdRef.current[applyBoxId] = newApplyingUri ?? undefined
|
||||
|
||||
rerender(c => c + 1)
|
||||
// rerender(c => c + 1)
|
||||
metricsService.capture('Apply Code', { length: codeStr.length }) // capture the length only
|
||||
}, [isDisabled, getStreamState, editCodeService, codeStr, uri, applyBoxId, metricsService])
|
||||
|
||||
|
||||
const onInterrupt = useCallback(() => {
|
||||
if (getStreamState() !== 'streaming') return
|
||||
const uri = getUriBeingApplied()
|
||||
const uri = getUriBeingApplied(applyBoxId)
|
||||
if (!uri) return
|
||||
|
||||
editCodeService.interruptURIStreaming({ uri })
|
||||
metricsService.capture('Stop Apply', {})
|
||||
}, [getStreamState, getUriBeingApplied, editCodeService, metricsService])
|
||||
}, [getStreamState, applyBoxId, editCodeService, metricsService])
|
||||
|
||||
const onAccept = useCallback(() => {
|
||||
const uri = getUriBeingApplied()
|
||||
const uri = getUriBeingApplied(applyBoxId)
|
||||
if (uri) editCodeService.acceptOrRejectAllDiffAreas({ uri, behavior: 'accept', removeCtrlKs: false })
|
||||
}, [getUriBeingApplied, editCodeService])
|
||||
}, [applyBoxId, editCodeService])
|
||||
|
||||
const onReject = useCallback(() => {
|
||||
const uri = getUriBeingApplied()
|
||||
const uri = getUriBeingApplied(applyBoxId)
|
||||
if (uri) editCodeService.acceptOrRejectAllDiffAreas({ uri, behavior: 'reject', removeCtrlKs: false })
|
||||
}, [getUriBeingApplied, editCodeService])
|
||||
}, [applyBoxId, editCodeService])
|
||||
|
||||
const onReapply = useCallback(() => {
|
||||
onReject()
|
||||
onClickSubmit()
|
||||
}, [onReject, onClickSubmit])
|
||||
// const onReapply = useCallback(() => {
|
||||
// onReject()
|
||||
// onClickSubmit()
|
||||
// }, [onReject, onClickSubmit])
|
||||
|
||||
const currStreamState = getStreamState()
|
||||
|
||||
const copyButton = (
|
||||
<CopyButton codeStr={codeStr} />
|
||||
)
|
||||
|
||||
const playButton = (
|
||||
<IconShell1
|
||||
Icon={Play}
|
||||
onClick={onClickSubmit}
|
||||
/>
|
||||
)
|
||||
|
||||
const stopButton = (
|
||||
<IconShell1
|
||||
Icon={Square}
|
||||
onClick={onInterrupt}
|
||||
/>
|
||||
)
|
||||
|
||||
const reapplyButton = (
|
||||
<IconShell1
|
||||
Icon={RotateCw}
|
||||
onClick={onReapply}
|
||||
/>
|
||||
)
|
||||
|
||||
const acceptButton = (
|
||||
<IconShell1
|
||||
Icon={Check}
|
||||
onClick={onAccept}
|
||||
className="text-green-600"
|
||||
/>
|
||||
)
|
||||
|
||||
const rejectButton = (
|
||||
<IconShell1
|
||||
Icon={X}
|
||||
onClick={onReject}
|
||||
className="text-red-600"
|
||||
/>
|
||||
)
|
||||
|
||||
|
||||
|
||||
let buttonsHTML = <></>
|
||||
|
||||
if (currStreamState === 'streaming') {
|
||||
buttonsHTML = <>
|
||||
<JumpToFileButton uri={uri} />
|
||||
{copyButton}
|
||||
{stopButton}
|
||||
</>
|
||||
return <IconShell1
|
||||
|
||||
Icon={Square}
|
||||
onClick={onInterrupt}
|
||||
|
||||
{...tooltipPropsForApplyBlock({ tooltipName: 'Stop' })}
|
||||
/>
|
||||
}
|
||||
|
||||
if (currStreamState === 'idle-no-changes') {
|
||||
buttonsHTML = <>
|
||||
<JumpToFileButton uri={uri} />
|
||||
{copyButton}
|
||||
{playButton}
|
||||
</>
|
||||
|
||||
return <IconShell1
|
||||
Icon={reapplyIcon ? RotateCw : Play}
|
||||
onClick={onClickSubmit}
|
||||
{...tooltipPropsForApplyBlock({ tooltipName: reapplyIcon ? 'Reapply' : 'Apply' })}
|
||||
/>
|
||||
}
|
||||
|
||||
if (currStreamState === 'idle-has-changes') {
|
||||
buttonsHTML = <>
|
||||
<JumpToFileButton uri={uri} />
|
||||
{reapplyButton}
|
||||
{rejectButton}
|
||||
{acceptButton}
|
||||
return <>
|
||||
{/* <IconShell1
|
||||
Icon={RotateCw}
|
||||
onClick={onReapply}
|
||||
/> */}
|
||||
<IconShell1
|
||||
Icon={X}
|
||||
onClick={onReject}
|
||||
{...tooltipPropsForApplyBlock({ tooltipName: 'Remove' })}
|
||||
/>
|
||||
<IconShell1
|
||||
Icon={Check}
|
||||
onClick={onAccept}
|
||||
{...tooltipPropsForApplyBlock({ tooltipName: 'Keep' })}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
|
||||
const statusIndicatorHTML = <div className='flex flex-row items-center size-4'>
|
||||
<div
|
||||
className={` size-1.5 rounded-full border
|
||||
${currStreamState === 'idle-no-changes' ? 'bg-void-bg-3 border-void-border-1' :
|
||||
currStreamState === 'streaming' ? 'bg-orange-500 border-orange-500 shadow-[0_0_4px_0px_rgba(234,88,12,0.6)]' :
|
||||
currStreamState === 'idle-has-changes' ? 'bg-green-500 border-green-500 shadow-[0_0_4px_0px_rgba(22,163,74,0.6)]' :
|
||||
'bg-void-border-1 border-void-border-1'
|
||||
}`
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
return {
|
||||
statusIndicatorHTML,
|
||||
buttonsHTML,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export const BlockCodeApplyWrapper = ({
|
||||
children,
|
||||
initValue,
|
||||
@@ -305,10 +348,10 @@ export const BlockCodeApplyWrapper = ({
|
||||
language: string;
|
||||
uri: URI | 'current',
|
||||
}) => {
|
||||
|
||||
const { statusIndicatorHTML, buttonsHTML } = useApplyButtonHTML({ codeStr: initValue, applyBoxId, uri })
|
||||
const accessor = useAccessor()
|
||||
const commandService = accessor.get('ICommandService')
|
||||
const { currStreamState } = useApplyButtonState({ applyBoxId, uri })
|
||||
|
||||
|
||||
const name = uri !== 'current' ?
|
||||
<ListableToolItem
|
||||
@@ -324,13 +367,15 @@ export const BlockCodeApplyWrapper = ({
|
||||
{/* header */}
|
||||
<div className=" select-none flex justify-between items-center py-1 px-2 border-b border-void-border-3 cursor-default">
|
||||
<div className="flex items-center">
|
||||
{statusIndicatorHTML}
|
||||
<StatusIndicatorForApplyButton uri={uri} applyBoxId={applyBoxId} />
|
||||
<span className="text-[13px] font-light text-void-fg-3">
|
||||
{name}
|
||||
</span>
|
||||
</div>
|
||||
<div className={`${canApply ? '' : 'hidden'} flex items-center gap-1`}>
|
||||
{buttonsHTML}
|
||||
<JumpToFileButton uri={uri} />
|
||||
{currStreamState === 'idle-no-changes' && <CopyButton codeStr={initValue} toolTipName='Copy' />}
|
||||
<ApplyButtonsHTML uri={uri} applyBoxId={applyBoxId} codeStr={initValue} reapplyIcon={false} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -31,6 +31,63 @@ function isValidUri(s: string): boolean {
|
||||
return s.length > 5 && isAbsolute(s) && !s.includes('//') && !s.includes('/*') // common case that is a false positive is comments like //
|
||||
}
|
||||
|
||||
// renders contiguous string of latex eg $e^{i\pi}$
|
||||
const LatexRender = ({ latex }: { latex: string }) => {
|
||||
return <span className="katex-error text-red-500">{latex}</span>
|
||||
// try {
|
||||
// let formula = latex;
|
||||
// let displayMode = false;
|
||||
|
||||
// // Extract the formula from delimiters
|
||||
// if (latex.startsWith('$') && latex.endsWith('$')) {
|
||||
// // Check if it's display math $$...$$
|
||||
// if (latex.startsWith('$$') && latex.endsWith('$$')) {
|
||||
// formula = latex.slice(2, -2);
|
||||
// displayMode = true;
|
||||
// } else {
|
||||
// formula = latex.slice(1, -1);
|
||||
// }
|
||||
// } else if (latex.startsWith('\\(') && latex.endsWith('\\)')) {
|
||||
// formula = latex.slice(2, -2);
|
||||
// } else if (latex.startsWith('\\[') && latex.endsWith('\\]')) {
|
||||
// formula = latex.slice(2, -2);
|
||||
// displayMode = true;
|
||||
// }
|
||||
|
||||
// // Render LaTeX
|
||||
// const html = katex.renderToString(formula, {
|
||||
// displayMode: displayMode,
|
||||
// throwOnError: false,
|
||||
// output: 'html'
|
||||
// });
|
||||
|
||||
// // Sanitize the HTML output with DOMPurify
|
||||
// const sanitizedHtml = dompurify.sanitize(html, {
|
||||
// RETURN_TRUSTED_TYPE: true,
|
||||
// USE_PROFILES: { html: true, svg: true, mathMl: true }
|
||||
// });
|
||||
|
||||
// // Add proper styling based on mode
|
||||
// const className = displayMode
|
||||
// ? 'katex-block my-2 text-center'
|
||||
// : 'katex-inline';
|
||||
|
||||
// // Use the ref approach to avoid dangerouslySetInnerHTML
|
||||
// const mathRef = React.useRef<HTMLSpanElement>(null);
|
||||
|
||||
// React.useEffect(() => {
|
||||
// if (mathRef.current) {
|
||||
// mathRef.current.innerHTML = sanitizedHtml as unknown as string;
|
||||
// }
|
||||
// }, [sanitizedHtml]);
|
||||
|
||||
// return <span ref={mathRef} className={className}></span>;
|
||||
// } catch (error) {
|
||||
// console.error('KaTeX rendering error:', error);
|
||||
// return <span className="katex-error text-red-500">{latex}</span>;
|
||||
// }
|
||||
}
|
||||
|
||||
const Codespan = ({ text, className, onClick }: { text: string, className?: string, onClick?: () => void }) => {
|
||||
|
||||
// TODO compute this once for efficiency. we should use `labels.ts/shorten` to display duplicates properly
|
||||
@@ -57,7 +114,7 @@ const CodespanWithLink = ({ text, rawText, chatMessageLocation }: { text: string
|
||||
const [didComputeCodespanLink, setDidComputeCodespanLink] = useState<boolean>(false)
|
||||
|
||||
let link = undefined
|
||||
if (rawText.endsWith("`")) { // if codespan was completed
|
||||
if (rawText.endsWith('`')) { // if codespan was completed
|
||||
|
||||
// get link from cache
|
||||
link = chatThreadService.getCodespanLink({ codespanStr: text, messageIdx, threadId })
|
||||
@@ -108,6 +165,105 @@ const CodespanWithLink = ({ text, rawText, chatMessageLocation }: { text: string
|
||||
}
|
||||
|
||||
|
||||
const paragraphToLatexSegments = (paragraphText: string) => {
|
||||
|
||||
const segments: React.ReactNode[] = [];
|
||||
|
||||
if (paragraphText
|
||||
&& !(paragraphText.includes('#') || paragraphText.includes('`')) // don't process latex if a codespan or header tag
|
||||
&& !/^[\w\s.()[\]{}]+$/.test(paragraphText) // don't process latex if string only contains alphanumeric chars, whitespace, periods, and brackets
|
||||
) {
|
||||
const rawText = paragraphText;
|
||||
// Regular expressions to match LaTeX delimiters
|
||||
const displayMathRegex = /\$\$(.*?)\$\$/g; // Display math: $$...$$
|
||||
const inlineMathRegex = /\$((?!\$).*?)\$/g; // Inline math: $...$ (but not $$)
|
||||
|
||||
// Check if the paragraph contains any LaTeX expressions
|
||||
if (displayMathRegex.test(rawText) || inlineMathRegex.test(rawText)) {
|
||||
// Reset the regex state (since we used .test earlier)
|
||||
displayMathRegex.lastIndex = 0;
|
||||
inlineMathRegex.lastIndex = 0;
|
||||
|
||||
// Parse the text into segments of regular text and LaTeX
|
||||
let lastIndex = 0;
|
||||
let segmentId = 0;
|
||||
|
||||
// First replace display math ($$...$$)
|
||||
let match;
|
||||
while ((match = displayMathRegex.exec(rawText)) !== null) {
|
||||
const [fullMatch, formula] = match;
|
||||
const matchIndex = match.index;
|
||||
|
||||
// Add text before the LaTeX expression
|
||||
if (matchIndex > lastIndex) {
|
||||
const textBefore = rawText.substring(lastIndex, matchIndex);
|
||||
segments.push(
|
||||
<span key={`text-${segmentId++}`}>
|
||||
{textBefore}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
// Add the LaTeX expression
|
||||
segments.push(
|
||||
<LatexRender key={`latex-${segmentId++}`} latex={fullMatch} />
|
||||
);
|
||||
|
||||
lastIndex = matchIndex + fullMatch.length;
|
||||
}
|
||||
|
||||
// Add any remaining text (which might contain inline math)
|
||||
if (lastIndex < rawText.length) {
|
||||
const remainingText = rawText.substring(lastIndex);
|
||||
|
||||
// Process inline math in the remaining text
|
||||
lastIndex = 0;
|
||||
inlineMathRegex.lastIndex = 0;
|
||||
const inlineSegments: React.ReactNode[] = [];
|
||||
|
||||
while ((match = inlineMathRegex.exec(remainingText)) !== null) {
|
||||
const [fullMatch] = match;
|
||||
const matchIndex = match.index;
|
||||
|
||||
// Add text before the inline LaTeX
|
||||
if (matchIndex > lastIndex) {
|
||||
const textBefore = remainingText.substring(lastIndex, matchIndex);
|
||||
inlineSegments.push(
|
||||
<span key={`inline-text-${segmentId++}`}>
|
||||
{textBefore}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
// Add the inline LaTeX
|
||||
inlineSegments.push(
|
||||
<LatexRender key={`inline-latex-${segmentId++}`} latex={fullMatch} />
|
||||
);
|
||||
|
||||
lastIndex = matchIndex + fullMatch.length;
|
||||
}
|
||||
|
||||
// Add any remaining text after all inline math
|
||||
if (lastIndex < remainingText.length) {
|
||||
inlineSegments.push(
|
||||
<span key={`inline-final-${segmentId++}`}>
|
||||
{remainingText.substring(lastIndex)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
segments.push(...inlineSegments);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return segments
|
||||
}
|
||||
|
||||
|
||||
export type RenderTokenOptions = { isApplyEnabled?: boolean, isLinkDetectionEnabled?: boolean }
|
||||
const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ...options }: { token: Token | string, inPTag?: boolean, codeURI?: URI, chatMessageLocation?: ChatMessageLocation, tokenIdx: string, } & RenderTokenOptions): React.ReactNode => {
|
||||
const accessor = useAccessor()
|
||||
@@ -120,11 +276,11 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
|
||||
return null;
|
||||
}
|
||||
|
||||
if (t.type === "space") {
|
||||
if (t.type === 'space') {
|
||||
return <span>{t.raw}</span>
|
||||
}
|
||||
|
||||
if (t.type === "code") {
|
||||
if (t.type === 'code') {
|
||||
const [firstLine, remainingContents] = separateOutFirstLine(t.text)
|
||||
const firstLineIsURI = isValidUri(firstLine) && !codeURI
|
||||
const contents = firstLineIsURI ? (remainingContents?.trimStart() || '') : t.text // exclude first-line URI from contents
|
||||
@@ -152,7 +308,7 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
|
||||
}
|
||||
|
||||
if (options.isApplyEnabled && chatMessageLocation) {
|
||||
const isCodeblockClosed = t.raw.trimEnd().endsWith('```') // user should only be able to Apply when the code has been closed (t.raw ends with "```")
|
||||
const isCodeblockClosed = t.raw.trimEnd().endsWith('```') // user should only be able to Apply when the code has been closed (t.raw ends with '```')
|
||||
|
||||
const applyBoxId = getApplyBoxId({
|
||||
threadId: chatMessageLocation.threadId,
|
||||
@@ -179,7 +335,7 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
|
||||
/>
|
||||
}
|
||||
|
||||
if (t.type === "heading") {
|
||||
if (t.type === 'heading') {
|
||||
|
||||
const HeadingTag = `h${t.depth}` as keyof JSX.IntrinsicElements
|
||||
|
||||
@@ -188,25 +344,26 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
|
||||
</HeadingTag>
|
||||
}
|
||||
|
||||
if (t.type === "table") {
|
||||
if (t.type === 'table') {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
{t.header.map((cell: any, index: number) => (
|
||||
<th key={index}>
|
||||
{cell.raw}
|
||||
{t.header.map((h, hIdx: number) => (
|
||||
<th key={hIdx}>
|
||||
{h.text}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{t.rows.map((row: any[], rowIndex: number) => (
|
||||
<tr key={rowIndex}>
|
||||
{row.map((cell: any, cellIndex: number) => (
|
||||
<td key={cellIndex} >
|
||||
{cell.raw}
|
||||
{t.rows.map((row, rowIdx: number) => (
|
||||
<tr key={rowIdx}>
|
||||
{row.map((r, rIdx: number) => (
|
||||
<td key={rIdx} >
|
||||
{r.text}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
@@ -217,14 +374,14 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
|
||||
)
|
||||
// return (
|
||||
// <div>
|
||||
// <table className={"min-w-full border border-void-bg-2"}>
|
||||
// <table className={'min-w-full border border-void-bg-2'}>
|
||||
// <thead>
|
||||
// <tr className="bg-void-bg-1">
|
||||
// <tr className='bg-void-bg-1'>
|
||||
// {t.header.map((cell: any, index: number) => (
|
||||
// <th
|
||||
// key={index}
|
||||
// className="px-4 py-2 border border-void-bg-2 font-semibold"
|
||||
// style={{ textAlign: t.align[index] || "left" }}
|
||||
// className='px-4 py-2 border border-void-bg-2 font-semibold'
|
||||
// style={{ textAlign: t.align[index] || 'left' }}
|
||||
// >
|
||||
// {cell.raw}
|
||||
// </th>
|
||||
@@ -237,8 +394,8 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
|
||||
// {row.map((cell: any, cellIndex: number) => (
|
||||
// <td
|
||||
// key={cellIndex}
|
||||
// className={"px-4 py-2 border border-void-bg-2"}
|
||||
// style={{ textAlign: t.align[cellIndex] || "left" }}
|
||||
// className={'px-4 py-2 border border-void-bg-2'}
|
||||
// style={{ textAlign: t.align[cellIndex] || 'left' }}
|
||||
// >
|
||||
// {cell.raw}
|
||||
// </td>
|
||||
@@ -251,32 +408,32 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
|
||||
// )
|
||||
}
|
||||
|
||||
if (t.type === "hr") {
|
||||
if (t.type === 'hr') {
|
||||
return <hr />
|
||||
}
|
||||
|
||||
if (t.type === "blockquote") {
|
||||
if (t.type === 'blockquote') {
|
||||
return <blockquote>{t.text}</blockquote>
|
||||
}
|
||||
|
||||
if (t.type === 'list_item') {
|
||||
return <li>
|
||||
<input type="checkbox" checked={t.checked} readOnly />
|
||||
<input type='checkbox' checked={t.checked} readOnly />
|
||||
<span>
|
||||
<ChatMarkdownRender chatMessageLocation={chatMessageLocation} string={t.text} inPTag={true} codeURI={codeURI} {...options} />
|
||||
</span>
|
||||
</li>
|
||||
}
|
||||
|
||||
if (t.type === "list") {
|
||||
const ListTag = t.ordered ? "ol" : "ul"
|
||||
if (t.type === 'list') {
|
||||
const ListTag = t.ordered ? 'ol' : 'ul'
|
||||
|
||||
return (
|
||||
<ListTag start={t.start ? t.start : undefined}>
|
||||
{t.items.map((item, index) => (
|
||||
<li key={index}>
|
||||
{item.task && (
|
||||
<input type="checkbox" checked={item.checked} readOnly />
|
||||
<input type='checkbox' checked={item.checked} readOnly />
|
||||
)}
|
||||
<span>
|
||||
<ChatMarkdownRender chatMessageLocation={chatMessageLocation} string={item.text} inPTag={true} {...options} />
|
||||
@@ -287,7 +444,18 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
|
||||
)
|
||||
}
|
||||
|
||||
if (t.type === "paragraph") {
|
||||
if (t.type === 'paragraph') {
|
||||
|
||||
// check for latex
|
||||
const latexSegments = paragraphToLatexSegments(t.raw)
|
||||
if (latexSegments.length !== 0) {
|
||||
if (inPTag) {
|
||||
return <span className='block'>{latexSegments}</span>;
|
||||
}
|
||||
return <p>{latexSegments}</p>;
|
||||
}
|
||||
|
||||
// if no latex, default behavior
|
||||
const contents = <>
|
||||
{t.tokens.map((token, index) => (
|
||||
<RenderToken key={index}
|
||||
@@ -304,34 +472,28 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
|
||||
return <p>{contents}</p>
|
||||
}
|
||||
|
||||
if (t.type === "html") {
|
||||
const contents = t.raw
|
||||
if (inPTag) return <span className='block'>{contents}</span>
|
||||
return <p>{contents}</p>
|
||||
}
|
||||
|
||||
if (t.type === "text" || t.type === "escape") {
|
||||
if (t.type === 'text' || t.type === 'escape' || t.type === 'html') {
|
||||
return <span>{t.raw}</span>
|
||||
}
|
||||
|
||||
if (t.type === "def") {
|
||||
if (t.type === 'def') {
|
||||
return <></> // Definitions are typically not rendered
|
||||
}
|
||||
|
||||
if (t.type === "link") {
|
||||
if (t.type === 'link') {
|
||||
return (
|
||||
<a
|
||||
onClick={() => { window.open(t.href) }}
|
||||
href={t.href}
|
||||
title={t.title ?? undefined}
|
||||
className='underline cursor-pointer hover:brightness-90 transition-all duration-200'
|
||||
className='underline cursor-pointer hover:brightness-90 transition-all duration-200 text-void-fg-2'
|
||||
>
|
||||
{t.text}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
if (t.type === "image") {
|
||||
if (t.type === 'image') {
|
||||
return <img
|
||||
src={t.href}
|
||||
alt={t.text}
|
||||
@@ -340,16 +502,16 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
|
||||
/>
|
||||
}
|
||||
|
||||
if (t.type === "strong") {
|
||||
if (t.type === 'strong') {
|
||||
return <strong>{t.text}</strong>
|
||||
}
|
||||
|
||||
if (t.type === "em") {
|
||||
if (t.type === 'em') {
|
||||
return <em>{t.text}</em>
|
||||
}
|
||||
|
||||
// inline code
|
||||
if (t.type === "codespan") {
|
||||
if (t.type === 'codespan') {
|
||||
|
||||
if (options.isLinkDetectionEnabled && chatMessageLocation) {
|
||||
return <CodespanWithLink
|
||||
@@ -363,18 +525,18 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
|
||||
return <Codespan text={t.text} />
|
||||
}
|
||||
|
||||
if (t.type === "br") {
|
||||
if (t.type === 'br') {
|
||||
return <br />
|
||||
}
|
||||
|
||||
// strikethrough
|
||||
if (t.type === "del") {
|
||||
if (t.type === 'del') {
|
||||
return <del>{t.text}</del>
|
||||
}
|
||||
// default
|
||||
return (
|
||||
<div className="bg-orange-50 rounded-sm overflow-hidden p-2">
|
||||
<span className="text-sm text-orange-500">Unknown token rendered...</span>
|
||||
<div className='bg-orange-50 rounded-sm overflow-hidden p-2'>
|
||||
<span className='text-sm text-orange-500'>Unknown token rendered...</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -390,4 +552,3 @@ export const ChatMarkdownRender = ({ string, inPTag = false, chatMessageLocation
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import { QuickEditPropsType } from '../../../quickEditActions.js';
|
||||
import { ButtonStop, ButtonSubmit, IconX, VoidChatArea } from '../sidebar-tsx/SidebarChat.js';
|
||||
import { VOID_CTRL_K_ACTION_ID } from '../../../actionIDs.js';
|
||||
import { useRefState } from '../util/helpers.js';
|
||||
import { useScrollbarStyles } from '../util/useScrollbarStyles.js';
|
||||
import { isFeatureNameDisabled } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js';
|
||||
|
||||
export const QuickEditChat = ({
|
||||
@@ -63,11 +62,14 @@ export const QuickEditChat = ({
|
||||
if (isStreamingRef.current) return
|
||||
textAreaFnsRef.current?.disable()
|
||||
|
||||
const [newApplyingUri, applyDonePromise] = await editCodeService.startApplying({
|
||||
const opts = {
|
||||
from: 'QuickEdit',
|
||||
diffareaid,
|
||||
startBehavior: 'keep-conflicts',
|
||||
}) ?? []
|
||||
} as const
|
||||
|
||||
await editCodeService.callBeforeStartApplying(opts)
|
||||
const [newApplyingUri, applyDonePromise] = editCodeService.startApplying(opts) ?? []
|
||||
// catch any errors by interrupting the stream
|
||||
applyDonePromise?.catch(e => { if (newApplyingUri) editCodeService.interruptCtrlKStreaming({ diffareaid }) })
|
||||
|
||||
@@ -86,8 +88,6 @@ export const QuickEditChat = ({
|
||||
editCodeService.removeCtrlKZone({ diffareaid })
|
||||
}, [editCodeService, diffareaid])
|
||||
|
||||
useScrollbarStyles(sizerRef)
|
||||
|
||||
const keybindingString = accessor.get('IKeybindingService').lookupKeybinding(VOID_CTRL_K_ACTION_ID)?.getLabel()
|
||||
|
||||
const chatAreaRef = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import React, { Component, ErrorInfo, ReactNode } from 'react';
|
||||
import { ErrorDisplay } from './ErrorDisplay.js';
|
||||
import { WarningBox } from '../void-settings-tsx/WarningBox.js';
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
@@ -51,11 +51,12 @@ class ErrorBoundary extends Component<Props, State> {
|
||||
|
||||
// Use ErrorDisplay component as the default error UI
|
||||
return (
|
||||
<ErrorDisplay
|
||||
message={this.state.error + ''}
|
||||
fullError={this.state.error}
|
||||
onDismiss={this.props.onDismiss || null}
|
||||
/>
|
||||
<WarningBox text={this.state.error + ''} />
|
||||
// <ErrorDisplay
|
||||
// message={this.state.error + ''}
|
||||
// fullError={this.state.error}
|
||||
// onDismiss={this.props.onDismiss || null}
|
||||
// />
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+368
-86
@@ -3,35 +3,20 @@
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import React from "react";
|
||||
import { useAccessor, useChatThreadsState } from '../util/services.js';
|
||||
import { ISidebarStateService } from '../../../sidebarStateService.js';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { CopyButton, IconShell1 } from '../markdown/ApplyBlockHoverButtons.js';
|
||||
import { useAccessor, useChatThreadsState, useChatThreadsStreamState, useFullChatThreadsStreamState, useSettingsState } from '../util/services.js';
|
||||
import { IconX } from './SidebarChat.js';
|
||||
import { Check, Copy, Icon, LoaderCircle, MessageCircleQuestion, Trash2, UserCheck, X } from 'lucide-react';
|
||||
import { IsRunningType, ThreadType } from '../../../chatThreadService.js';
|
||||
|
||||
|
||||
const truncate = (s: string) => {
|
||||
let len = s.length
|
||||
const TRUNC_AFTER = 16
|
||||
if (len >= TRUNC_AFTER)
|
||||
s = s.substring(0, TRUNC_AFTER) + '...'
|
||||
return s
|
||||
}
|
||||
export const OldSidebarThreadSelector = () => {
|
||||
|
||||
|
||||
export const SidebarThreadSelector = () => {
|
||||
const threadsState = useChatThreadsState()
|
||||
|
||||
const accessor = useAccessor()
|
||||
const chatThreadsService = accessor.get('IChatThreadService')
|
||||
const sidebarStateService = accessor.get('ISidebarStateService')
|
||||
|
||||
const { allThreads } = threadsState
|
||||
|
||||
// sorted by most recent to least recent
|
||||
const sortedThreadIds = Object.keys(allThreads ?? {})
|
||||
.sort((threadId1, threadId2) => (allThreads[threadId1]?.lastModified ?? 0) > (allThreads[threadId2]?.lastModified ?? 0) ? -1 : 1)
|
||||
.filter(threadId => (allThreads![threadId]?.messages.length ?? 0) !== 0)
|
||||
|
||||
return (
|
||||
<div className="flex p-2 flex-col gap-y-1 max-h-[200px] overflow-y-auto">
|
||||
|
||||
@@ -52,72 +37,369 @@ export const SidebarThreadSelector = () => {
|
||||
</div>
|
||||
|
||||
{/* a list of all the past threads */}
|
||||
<div className="px-1">
|
||||
<ul className="flex flex-col gap-y-0.5 overflow-y-auto list-disc">
|
||||
|
||||
{sortedThreadIds.length === 0
|
||||
|
||||
? <div key="nothreads" className="text-center text-void-fg-3 brightness-90 text-sm">{`There are no chat threads yet.`}</div>
|
||||
|
||||
: sortedThreadIds.map((threadId) => {
|
||||
if (!allThreads) {
|
||||
return <li key="error" className="text-void-warning">{`Error accessing chat history.`}</li>;
|
||||
}
|
||||
const pastThread = allThreads[threadId];
|
||||
if (!pastThread) {
|
||||
return <li key="error" className="text-void-warning">{`Error accessing chat history.`}</li>;
|
||||
}
|
||||
|
||||
|
||||
let firstMsg = null;
|
||||
// let secondMsg = null;
|
||||
|
||||
const firstUserMsgIdx = pastThread.messages.findIndex((msg) => msg.role !== 'tool' && msg.role !== 'tool_request');
|
||||
|
||||
if (firstUserMsgIdx !== -1) {
|
||||
// firstMsg = truncate(pastThread.messages[firstMsgIdx].displayContent ?? '');
|
||||
const firsUsertMsgObj = pastThread.messages[firstUserMsgIdx]
|
||||
firstMsg = firsUsertMsgObj.role === 'user' && firsUsertMsgObj.displayContent || '';
|
||||
} else {
|
||||
firstMsg = '""';
|
||||
}
|
||||
|
||||
// const secondMsgIdx = pastThread.messages.findIndex(
|
||||
// (msg, i) => msg.role !== 'system' && !!msg.displayContent && i > firstMsgIdx
|
||||
// );
|
||||
|
||||
// if (secondMsgIdx !== -1) {
|
||||
// secondMsg = truncate(pastThread.messages[secondMsgIdx].displayContent ?? '');
|
||||
// }
|
||||
|
||||
const numMessages = pastThread.messages.filter((msg) => msg.role !== 'tool_request').length;
|
||||
|
||||
return (
|
||||
<li key={pastThread.id}>
|
||||
<button
|
||||
type='button'
|
||||
className={`
|
||||
hover:bg-void-bg-1
|
||||
${threadsState.currentThreadId === pastThread.id ? 'bg-void-bg-1' : ''}
|
||||
rounded-sm px-2 py-1
|
||||
w-full
|
||||
text-left
|
||||
flex items-center
|
||||
`}
|
||||
onClick={() => chatThreadsService.switchToThread(pastThread.id)}
|
||||
onDoubleClick={() => sidebarStateService.setState({ isHistoryOpen: false })}
|
||||
title={new Date(pastThread.lastModified).toLocaleString()}
|
||||
>
|
||||
<div className='truncate'>{`${firstMsg}`}</div>
|
||||
<div>{`\u00A0(${numMessages})`}</div>
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
{/* <OldPastThreadsList /> */}
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const truncate = (s: string) => {
|
||||
let len = s.length
|
||||
const TRUNC_AFTER = 16
|
||||
if (len >= TRUNC_AFTER)
|
||||
s = s.substring(0, TRUNC_AFTER) + '...'
|
||||
return s
|
||||
}
|
||||
|
||||
|
||||
|
||||
const OldPastThreadsList = () => {
|
||||
|
||||
const accessor = useAccessor()
|
||||
const chatThreadsService = accessor.get('IChatThreadService')
|
||||
const sidebarStateService = accessor.get('ISidebarStateService')
|
||||
|
||||
const threadsState = useChatThreadsState()
|
||||
const { allThreads } = threadsState
|
||||
|
||||
// sorted by most recent to least recent
|
||||
const sortedThreadIds = Object.keys(allThreads ?? {})
|
||||
.sort((threadId1, threadId2) => (allThreads[threadId1]?.lastModified ?? 0) > (allThreads[threadId2]?.lastModified ?? 0) ? -1 : 1)
|
||||
.filter(threadId => (allThreads![threadId]?.messages.length ?? 0) !== 0)
|
||||
|
||||
|
||||
return <div className="px-1">
|
||||
<ul className="flex flex-col gap-y-0.5 overflow-y-auto list-disc">
|
||||
|
||||
{sortedThreadIds.length === 0
|
||||
|
||||
? <div key="nothreads" className="text-center text-void-fg-3 brightness-90 text-root">{`There are no chat threads yet.`}</div>
|
||||
|
||||
: sortedThreadIds.map((threadId) => {
|
||||
if (!allThreads) {
|
||||
return <li key="error" className="text-void-warning">{`Error accessing chat history.`}</li>;
|
||||
}
|
||||
const pastThread = allThreads[threadId];
|
||||
if (!pastThread) {
|
||||
return <li key="error" className="text-void-warning">{`Error accessing chat history.`}</li>;
|
||||
}
|
||||
|
||||
|
||||
let firstMsg = null;
|
||||
// let secondMsg = null;
|
||||
|
||||
const firstUserMsgIdx = pastThread.messages.findIndex((msg) => msg.role === 'user');
|
||||
|
||||
if (firstUserMsgIdx !== -1) {
|
||||
// firstMsg = truncate(pastThread.messages[firstMsgIdx].displayContent ?? '');
|
||||
const firsUsertMsgObj = pastThread.messages[firstUserMsgIdx]
|
||||
firstMsg = firsUsertMsgObj.role === 'user' && firsUsertMsgObj.displayContent || '';
|
||||
} else {
|
||||
firstMsg = '""';
|
||||
}
|
||||
|
||||
// const secondMsgIdx = pastThread.messages.findIndex(
|
||||
// (msg, i) => msg.role !== 'system' && !!msg.displayContent && i > firstMsgIdx
|
||||
// );
|
||||
|
||||
// if (secondMsgIdx !== -1) {
|
||||
// secondMsg = truncate(pastThread.messages[secondMsgIdx].displayContent ?? '');
|
||||
// }
|
||||
|
||||
const numMessages = pastThread.messages.filter((msg) => msg.role === 'assistant' || msg.role === 'user').length;
|
||||
|
||||
return (
|
||||
<li key={pastThread.id}>
|
||||
<button
|
||||
type='button'
|
||||
className={`
|
||||
hover:bg-void-bg-1
|
||||
${threadsState.currentThreadId === pastThread.id ? 'bg-void-bg-1' : ''}
|
||||
rounded-sm px-2 py-1
|
||||
w-full
|
||||
text-left
|
||||
flex items-center
|
||||
`}
|
||||
onClick={() => {
|
||||
chatThreadsService.switchToThread(pastThread.id);
|
||||
sidebarStateService.setState({ isHistoryOpen: false })
|
||||
}}
|
||||
title={new Date(pastThread.lastModified).toLocaleString()}
|
||||
>
|
||||
<div className='truncate'>{`${firstMsg}`}</div>
|
||||
<div>{`\u00A0(${numMessages})`}</div>
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
const numInitialThreads = 3
|
||||
|
||||
export const PastThreadsList = ({ className = '' }: { className?: string }) => {
|
||||
const [showAll, setShowAll] = useState(false);
|
||||
|
||||
const [hoveredIdx, setHoveredIdx] = useState<number | null>(null)
|
||||
|
||||
const threadsState = useChatThreadsState()
|
||||
const { allThreads } = threadsState
|
||||
|
||||
const streamState = useFullChatThreadsStreamState()
|
||||
|
||||
const runningThreadIds: { [threadId: string]: IsRunningType | undefined } = {}
|
||||
for (const threadId in streamState) {
|
||||
const isRunning = streamState[threadId]?.isRunning
|
||||
if (isRunning) { runningThreadIds[threadId] = isRunning }
|
||||
}
|
||||
|
||||
if (!allThreads) {
|
||||
return <div key="error" className="p-1">{`Error accessing chat history.`}</div>;
|
||||
}
|
||||
|
||||
// sorted by most recent to least recent
|
||||
const sortedThreadIds = Object.keys(allThreads ?? {})
|
||||
.sort((threadId1, threadId2) => (allThreads[threadId1]?.lastModified ?? 0) > (allThreads[threadId2]?.lastModified ?? 0) ? -1 : 1)
|
||||
.filter(threadId => (allThreads![threadId]?.messages.length ?? 0) !== 0)
|
||||
|
||||
// Get only first 5 threads if not showing all
|
||||
const hasMoreThreads = sortedThreadIds.length > numInitialThreads;
|
||||
const displayThreads = showAll ? sortedThreadIds : sortedThreadIds.slice(0, numInitialThreads);
|
||||
|
||||
return (
|
||||
<div className={`flex flex-col mb-2 gap-2 w-full text-nowrap text-void-fg-3 select-none relative ${className}`}>
|
||||
{displayThreads.length === 0
|
||||
? <></> // No chats yet... Suggestion: Tell me about my codebase Suggestion: Create a new .voidrules file in the root of my repo
|
||||
: displayThreads.map((threadId, i) => {
|
||||
const pastThread = allThreads[threadId];
|
||||
if (!pastThread) {
|
||||
return <div key={i} className="p-1">{`Error accessing chat history.`}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<PastThreadElement
|
||||
key={pastThread.id}
|
||||
pastThread={pastThread}
|
||||
idx={i}
|
||||
hoveredIdx={hoveredIdx}
|
||||
setHoveredIdx={setHoveredIdx}
|
||||
isRunning={runningThreadIds[pastThread.id]}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
{hasMoreThreads && !showAll && (
|
||||
<div
|
||||
className="text-void-fg-3 opacity-60 hover:opacity-100 hover:brightness-115 cursor-pointer p-1 text-xs"
|
||||
onClick={() => setShowAll(true)}
|
||||
>
|
||||
Show {sortedThreadIds.length - numInitialThreads} more...
|
||||
</div>
|
||||
)}
|
||||
{hasMoreThreads && showAll && (
|
||||
<div
|
||||
className="text-void-fg-3 opacity-60 hover:opacity-100 hover:brightness-115 cursor-pointer p-1 text-xs"
|
||||
onClick={() => setShowAll(false)}
|
||||
>
|
||||
Show less
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// Format date to display as today, yesterday, or date
|
||||
const formatDate = (date: Date) => {
|
||||
const now = new Date();
|
||||
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
const yesterday = new Date(today);
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
|
||||
if (date >= today) {
|
||||
return 'Today';
|
||||
} else if (date >= yesterday) {
|
||||
return 'Yesterday';
|
||||
} else {
|
||||
return `${date.toLocaleString('default', { month: 'short' })} ${date.getDate()}`;
|
||||
}
|
||||
};
|
||||
|
||||
// Format time to 12-hour format
|
||||
const formatTime = (date: Date) => {
|
||||
return date.toLocaleString('en-US', {
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
hour12: true
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const DuplicateButton = ({ threadId }: { threadId: string }) => {
|
||||
const accessor = useAccessor()
|
||||
const chatThreadsService = accessor.get('IChatThreadService')
|
||||
return <IconShell1
|
||||
Icon={Copy}
|
||||
className='size-[11px]'
|
||||
onClick={() => { chatThreadsService.duplicateThread(threadId); }}
|
||||
data-tooltip-id='void-tooltip'
|
||||
data-tooltip-place='top'
|
||||
data-tooltip-content='Duplicate thread'
|
||||
>
|
||||
</IconShell1>
|
||||
|
||||
}
|
||||
|
||||
const TrashButton = ({ threadId }: { threadId: string }) => {
|
||||
|
||||
const accessor = useAccessor()
|
||||
const chatThreadsService = accessor.get('IChatThreadService')
|
||||
|
||||
|
||||
const [isTrashPressed, setIsTrashPressed] = useState(false)
|
||||
|
||||
return (isTrashPressed ?
|
||||
<div className='flex flex-nowrap text-nowrap gap-1'>
|
||||
<IconShell1
|
||||
Icon={X}
|
||||
className='size-[11px]'
|
||||
onClick={() => { setIsTrashPressed(false); }}
|
||||
data-tooltip-id='void-tooltip'
|
||||
data-tooltip-place='top'
|
||||
data-tooltip-content='Cancel'
|
||||
/>
|
||||
<IconShell1
|
||||
Icon={Check}
|
||||
className='size-[11px]'
|
||||
onClick={() => { chatThreadsService.deleteThread(threadId); setIsTrashPressed(false); }}
|
||||
data-tooltip-id='void-tooltip'
|
||||
data-tooltip-place='top'
|
||||
data-tooltip-content='Confirm'
|
||||
/>
|
||||
</div>
|
||||
: <IconShell1
|
||||
Icon={Trash2}
|
||||
className='size-[11px]'
|
||||
onClick={() => { setIsTrashPressed(true); }}
|
||||
data-tooltip-id='void-tooltip'
|
||||
data-tooltip-place='top'
|
||||
data-tooltip-content='Delete thread'
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const PastThreadElement = ({ pastThread, idx, hoveredIdx, setHoveredIdx, isRunning }: {
|
||||
pastThread: ThreadType,
|
||||
idx: number,
|
||||
hoveredIdx: number | null,
|
||||
setHoveredIdx: (idx: number | null) => void,
|
||||
isRunning: IsRunningType | undefined,
|
||||
}
|
||||
|
||||
) => {
|
||||
|
||||
|
||||
const accessor = useAccessor()
|
||||
const chatThreadsService = accessor.get('IChatThreadService')
|
||||
const sidebarStateService = accessor.get('ISidebarStateService')
|
||||
|
||||
// const settingsState = useSettingsState()
|
||||
// const convertService = accessor.get('IConvertToLLMMessageService')
|
||||
// const chatMode = settingsState.globalSettings.chatMode
|
||||
// const modelSelection = settingsState.modelSelectionOfFeature?.Chat ?? null
|
||||
// const copyChatButton = <CopyButton
|
||||
// codeStr={async () => {
|
||||
// const { messages } = await convertService.prepareLLMChatMessages({
|
||||
// chatMessages: currentThread.messages,
|
||||
// chatMode,
|
||||
// modelSelection,
|
||||
// })
|
||||
// return JSON.stringify(messages, null, 2)
|
||||
// }}
|
||||
// toolTipName={modelSelection === null ? 'Copy As Messages Payload' : `Copy As ${displayInfoOfProviderName(modelSelection.providerName).title} Payload`}
|
||||
// />
|
||||
|
||||
|
||||
// const currentThread = chatThreadsService.getCurrentThread()
|
||||
// const copyChatButton2 = <CopyButton
|
||||
// codeStr={async () => {
|
||||
// return JSON.stringify(currentThread.messages, null, 2)
|
||||
// }}
|
||||
// toolTipName={`Copy As Void Chat`}
|
||||
// />
|
||||
|
||||
let firstMsg = null;
|
||||
const firstUserMsgIdx = pastThread.messages.findIndex((msg) => msg.role === 'user');
|
||||
|
||||
if (firstUserMsgIdx !== -1) {
|
||||
const firsUsertMsgObj = pastThread.messages[firstUserMsgIdx];
|
||||
firstMsg = firsUsertMsgObj.role === 'user' && firsUsertMsgObj.displayContent || '';
|
||||
} else {
|
||||
firstMsg = '""';
|
||||
}
|
||||
|
||||
const numMessages = pastThread.messages.filter((msg) => msg.role === 'assistant' || msg.role === 'user').length;
|
||||
|
||||
const detailsHTML = <span
|
||||
className='gap-1 inline-flex items-center'
|
||||
// data-tooltip-id='void-tooltip'
|
||||
// data-tooltip-content={`Last modified ${formatTime(new Date(pastThread.lastModified))}`}
|
||||
// data-tooltip-place='top'
|
||||
>
|
||||
{/* <span>{numMessages}</span> */}
|
||||
{formatDate(new Date(pastThread.lastModified))}
|
||||
</span>
|
||||
|
||||
return <div
|
||||
key={pastThread.id}
|
||||
className={`
|
||||
py-1 px-2 rounded text-sm bg-zinc-700/5 hover:bg-zinc-700/10 dark:bg-zinc-300/5 dark:hover:bg-zinc-300/10 cursor-pointer opacity-80 hover:opacity-100
|
||||
`}
|
||||
onClick={() => {
|
||||
chatThreadsService.switchToThread(pastThread.id);
|
||||
sidebarStateService.setState({ isHistoryOpen: false });
|
||||
}}
|
||||
onMouseEnter={() => setHoveredIdx(idx)}
|
||||
onMouseLeave={() => setHoveredIdx(null)}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-1">
|
||||
<span className="flex items-center gap-2 min-w-0 overflow-hidden">
|
||||
{/* spinner */}
|
||||
{isRunning === 'LLM' || isRunning === 'tool' ? <LoaderCircle className="animate-spin bg-void-stroke-1 flex-shrink-0 flex-grow-0" size={14} />
|
||||
:
|
||||
isRunning === 'awaiting_user' ? <MessageCircleQuestion className="bg-void-stroke-1 flex-shrink-0 flex-grow-0" size={14} />
|
||||
:
|
||||
null}
|
||||
{/* name */}
|
||||
<span className="truncate overflow-hidden text-ellipsis">{firstMsg}</span>
|
||||
</span>
|
||||
|
||||
<div className="flex items-center gap-x-1 opacity-60">
|
||||
{idx === hoveredIdx ?
|
||||
<>
|
||||
{/* trash icon */}
|
||||
<DuplicateButton threadId={pastThread.id} />
|
||||
|
||||
{/* trash icon */}
|
||||
<TrashButton threadId={pastThread.id} />
|
||||
</>
|
||||
: <>
|
||||
{detailsHTML}
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
--void-bg-1: var(--vscode-input-background);
|
||||
--void-bg-1-alt: var(--vscode-badge-background);
|
||||
--void-bg-2: var(--vscode-sideBar-background);
|
||||
--void-bg-2-alt: color-mix(in srgb, var(--vscode-sideBar-background) 30%, var(--vscode-editor-background) 70%);
|
||||
--void-bg-2-alt: color-mix(in srgb, var(--vscode-editor-background) 30%, var(--vscode-sideBar-background) 70%);
|
||||
--void-bg-2-hover: color-mix(in srgb, var(--vscode-editor-foreground) 2%, var(--vscode-sideBar-background) 98%);
|
||||
--void-bg-3: var(--vscode-editor-background);
|
||||
|
||||
--void-fg-0: color-mix(in srgb, var(--vscode-tab-activeForeground) 90%, black 10%);
|
||||
|
||||
@@ -48,6 +48,80 @@ export const WidgetComponent = <CtorParams extends any[], Instance>({ ctor, prop
|
||||
return <div ref={containerRef} className={className === undefined ? `w-full` : className}>{children}</div>
|
||||
}
|
||||
|
||||
type GenerateNextOptions = (newPathText: string) => Option[]
|
||||
|
||||
type Option = {
|
||||
name: string,
|
||||
displayName: string,
|
||||
} & (
|
||||
| { nextOptions: Option[], generateNextOptions?: undefined }
|
||||
| { nextOptions?: undefined, generateNextOptions: GenerateNextOptions }
|
||||
| { nextOptions?: undefined, generateNextOptions?: undefined }
|
||||
)
|
||||
|
||||
|
||||
const getOptionsAtPath = (accessor: ReturnType<typeof useAccessor>, path: string[], newPathText: string) => {
|
||||
|
||||
|
||||
const allOptions: Option[] = [
|
||||
{
|
||||
name: 'files',
|
||||
displayName: 'files',
|
||||
generateNextOptions: () => [
|
||||
{ name: 'a.txt', displayName: 'a.txt', },
|
||||
{ name: 'b.txt', displayName: 'b.txt', },
|
||||
{ name: 'c.txt', displayName: 'c.txt', },
|
||||
{ name: 'd.txt', displayName: 'd.txt', },
|
||||
{ name: 'e.txt', displayName: 'e.txt', },
|
||||
{ name: 'f.txt', displayName: 'f.txt', },
|
||||
{ name: 'g.txt', displayName: 'g.txt', },
|
||||
{ name: '!a.txt', displayName: '!a.txt', },
|
||||
{ name: '!b.txt', displayName: '!b.txt', },
|
||||
{ name: '!c.txt', displayName: '!c.txt', },
|
||||
{ name: '!d.txt', displayName: '!d.txt', },
|
||||
{ name: '!e.txt', displayName: '!e.txt', },
|
||||
{ name: '!f.txt', displayName: '!f.txt', },
|
||||
{ name: '!g.txt', displayName: '!g.txt', },
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'folders',
|
||||
displayName: 'folders',
|
||||
nextOptions: [
|
||||
{ name: 'FOLDER', displayName: 'FOLDER', },
|
||||
]
|
||||
},
|
||||
]
|
||||
|
||||
// follow the path in the optionsTree (until the last path element)
|
||||
|
||||
let nextOptionsAtPath = allOptions
|
||||
let generateNextOptionsAtPath: GenerateNextOptions | undefined = undefined
|
||||
|
||||
for (const pn of path) {
|
||||
|
||||
const selectedOption = nextOptionsAtPath.find(o => o.name.toLowerCase() === pn.toLowerCase())
|
||||
|
||||
if (!selectedOption) return;
|
||||
|
||||
nextOptionsAtPath = selectedOption.nextOptions! // assume nextOptions exists until we hit the very last option (the path will never contain the last possible option)
|
||||
generateNextOptionsAtPath = selectedOption.generateNextOptions
|
||||
|
||||
}
|
||||
|
||||
|
||||
if (generateNextOptionsAtPath) {
|
||||
nextOptionsAtPath = generateNextOptionsAtPath(newPathText)
|
||||
}
|
||||
|
||||
const optionsAtPath = nextOptionsAtPath.filter(o => o.name.includes(newPathText))
|
||||
|
||||
|
||||
return optionsAtPath
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
export type TextAreaFns = { setValue: (v: string) => void, enable: () => void, disable: () => void }
|
||||
type InputBox2Props = {
|
||||
@@ -64,8 +138,235 @@ type InputBox2Props = {
|
||||
}
|
||||
export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(function X({ initValue, placeholder, multiline, fnsRef, className, onKeyDown, onFocus, onBlur, onChangeText }, ref) {
|
||||
|
||||
|
||||
// mirrors whatever is in ref
|
||||
const accessor = useAccessor()
|
||||
const toolsService = accessor.get('IToolsService')
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const textAreaRef = useRef<HTMLTextAreaElement | null>(null)
|
||||
const selectedOptionRef = useRef<HTMLDivElement>(null);
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
|
||||
const [path, setPath] = useState<string[]>([]);
|
||||
const [optionIdx, setOptionIdx] = useState<number>(0);
|
||||
const [options, setOptions] = useState<Option[]>([]);
|
||||
const [newPathText, setNewPathText] = useState<string>('');
|
||||
|
||||
|
||||
const insertTextAtCursor = (text: string) => {
|
||||
const textarea = textAreaRef.current;
|
||||
if (!textarea) return;
|
||||
|
||||
// Focus the textarea first
|
||||
textarea.focus();
|
||||
|
||||
// The most reliable way to simulate typing is to use execCommand
|
||||
// which will trigger all the appropriate native events
|
||||
document.execCommand('insertText', false, text);
|
||||
|
||||
// React's onChange relies on a SyntheticEvent system
|
||||
// The best way to ensure it runs is to call callbacks directly
|
||||
if (onChangeText) {
|
||||
onChangeText(textarea.value);
|
||||
}
|
||||
adjustHeight();
|
||||
};
|
||||
|
||||
|
||||
|
||||
const onSelectOption = () => {
|
||||
|
||||
if (!options.length) { return; }
|
||||
|
||||
const option = options[optionIdx];
|
||||
const newPath = [...path, option.name]
|
||||
const isLastOption = !option.generateNextOptions && !option.nextOptions
|
||||
|
||||
setPath(newPath)
|
||||
setNewPathText('')
|
||||
setOptionIdx(0)
|
||||
if (isLastOption) {
|
||||
setIsMenuOpen(false)
|
||||
insertTextAtCursor(`TODO-${option.displayName}`)
|
||||
}
|
||||
else {
|
||||
setOptions(getOptionsAtPath(accessor, newPath, '') || [])
|
||||
}
|
||||
}
|
||||
|
||||
const onRemoveOption = () => {
|
||||
const newPath = [...path.slice(0, path.length - 1)]
|
||||
setPath(newPath)
|
||||
setNewPathText('')
|
||||
setOptionIdx(0)
|
||||
setOptions(getOptionsAtPath(accessor, newPath, '') || [])
|
||||
}
|
||||
|
||||
const onOpenOptionMenu = () => {
|
||||
setPath([])
|
||||
setNewPathText('')
|
||||
setIsMenuOpen(true);
|
||||
setOptionIdx(0);
|
||||
setOptions(getOptionsAtPath(accessor, [], '') || []);
|
||||
}
|
||||
const onCloseOptionMenu = () => {
|
||||
setIsMenuOpen(false);
|
||||
}
|
||||
|
||||
const onNavigateUp = () => {
|
||||
if (options.length === 0) return;
|
||||
setOptionIdx((prevIdx) => (prevIdx - 1 + options.length) % options.length);
|
||||
}
|
||||
const onNavigateDown = () => {
|
||||
if (options.length === 0) return;
|
||||
setOptionIdx((prevIdx) => (prevIdx + 1) % options.length);
|
||||
}
|
||||
|
||||
const onPathTextChange = (newStr: string) => {
|
||||
setNewPathText(newStr);
|
||||
setOptions(getOptionsAtPath(accessor, path, newStr) || []);
|
||||
|
||||
}
|
||||
|
||||
const onMenuKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === 'ArrowUp') {
|
||||
onNavigateUp();
|
||||
} else if (e.key === 'ArrowDown') {
|
||||
onNavigateDown();
|
||||
} else if (e.key === 'ArrowLeft') {
|
||||
onSelectOption();
|
||||
} else if (e.key === 'ArrowRight') {
|
||||
onSelectOption();
|
||||
} else if (e.key === 'Enter') {
|
||||
onSelectOption();
|
||||
} else if (e.key === 'Escape') {
|
||||
onCloseOptionMenu()
|
||||
} else if (e.key === 'Backspace') {
|
||||
|
||||
if (!newPathText) { // No text remaining
|
||||
if (path.length === 0) {
|
||||
onCloseOptionMenu()
|
||||
} else {
|
||||
onRemoveOption();
|
||||
}
|
||||
}
|
||||
else if (e.altKey || e.ctrlKey || e.metaKey) { // Ctrl+Backspace
|
||||
onPathTextChange('')
|
||||
}
|
||||
else { // Backspace
|
||||
onPathTextChange(newPathText.slice(0, -1))
|
||||
}
|
||||
} else if (e.key.length === 1) {
|
||||
if (e.altKey || e.ctrlKey || e.metaKey) { // Ctrl+letter
|
||||
// do nothing
|
||||
}
|
||||
else { // letter
|
||||
onPathTextChange(newPathText + e.key)
|
||||
}
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
};
|
||||
|
||||
// scroll the selected optionIdx into view on optionIdx and newPathText changes
|
||||
useEffect(() => {
|
||||
if (isMenuOpen && selectedOptionRef.current) {
|
||||
selectedOptionRef.current.scrollIntoView({
|
||||
behavior: 'instant',
|
||||
block: 'nearest',
|
||||
inline: 'nearest',
|
||||
});
|
||||
}
|
||||
}, [optionIdx, isMenuOpen, newPathText, selectedOptionRef]);
|
||||
|
||||
|
||||
|
||||
const measureRef = useRef<HTMLDivElement>(null);
|
||||
const gapPx = 2
|
||||
const offsetPx = 2
|
||||
const {
|
||||
x,
|
||||
y,
|
||||
strategy,
|
||||
refs,
|
||||
middlewareData,
|
||||
update
|
||||
} = useFloating({
|
||||
open: isMenuOpen,
|
||||
onOpenChange: setIsMenuOpen,
|
||||
placement: 'top',
|
||||
|
||||
middleware: [
|
||||
offset({ mainAxis: gapPx, crossAxis: offsetPx }),
|
||||
flip({
|
||||
boundary: document.body,
|
||||
padding: 8
|
||||
}),
|
||||
shift({
|
||||
boundary: document.body,
|
||||
padding: 8,
|
||||
}),
|
||||
size({
|
||||
apply({ availableHeight, elements, rects }) {
|
||||
const maxHeight = Math.min(availableHeight)
|
||||
|
||||
Object.assign(elements.floating.style, {
|
||||
maxHeight: `${maxHeight}px`,
|
||||
overflowY: 'auto',
|
||||
// Ensure the width isn't constrained by the parent
|
||||
width: `${Math.max(
|
||||
rects.reference.width,
|
||||
measureRef.current?.offsetWidth ?? 0
|
||||
)}px`
|
||||
});
|
||||
},
|
||||
padding: 8,
|
||||
// Use viewport as boundary instead of any parent element
|
||||
boundary: document.body,
|
||||
}),
|
||||
],
|
||||
whileElementsMounted: autoUpdate,
|
||||
strategy: 'fixed',
|
||||
});
|
||||
useEffect(() => {
|
||||
if (!isMenuOpen) return;
|
||||
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
const target = event.target as Node;
|
||||
const floating = refs.floating.current;
|
||||
const reference = refs.reference.current;
|
||||
|
||||
// Check if reference is an HTML element before using contains
|
||||
const isReferenceHTMLElement = reference && 'contains' in reference;
|
||||
|
||||
if (
|
||||
floating &&
|
||||
(!isReferenceHTMLElement || !reference.contains(target)) &&
|
||||
!floating.contains(target)
|
||||
) {
|
||||
setIsMenuOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, [isMenuOpen, refs.floating, refs.reference]);
|
||||
|
||||
|
||||
|
||||
const [isEnabled, setEnabled] = useState(true)
|
||||
|
||||
const adjustHeight = useCallback(() => {
|
||||
@@ -104,17 +405,20 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
|
||||
|
||||
|
||||
|
||||
return (
|
||||
return <>
|
||||
<textarea
|
||||
autoFocus={false}
|
||||
ref={useCallback((r: HTMLTextAreaElement | null) => {
|
||||
if (fnsRef)
|
||||
fnsRef.current = fns
|
||||
|
||||
refs.setReference(r)
|
||||
|
||||
textAreaRef.current = r
|
||||
if (typeof ref === 'function') ref(r)
|
||||
else if (ref) ref.current = r
|
||||
adjustHeight()
|
||||
}, [fnsRef, fns, setEnabled, adjustHeight, ref])}
|
||||
}, [fnsRef, fns, setEnabled, adjustHeight, ref, refs])}
|
||||
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
@@ -129,7 +433,16 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
|
||||
// inputBorder: asCssVariable(inputBorder),
|
||||
}}
|
||||
|
||||
onChange={useCallback(() => {
|
||||
onInput={useCallback((event: React.FormEvent<HTMLTextAreaElement>) => {
|
||||
const latestChange = (event.nativeEvent as InputEvent).data;
|
||||
|
||||
if (latestChange === '@') {
|
||||
onOpenOptionMenu()
|
||||
}
|
||||
|
||||
}, [onOpenOptionMenu, accessor])}
|
||||
|
||||
onChange={useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const r = textAreaRef.current
|
||||
if (!r) return
|
||||
onChangeText?.(r.value)
|
||||
@@ -137,21 +450,114 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
|
||||
}, [onChangeText, adjustHeight])}
|
||||
|
||||
onKeyDown={useCallback((e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
|
||||
if (isMenuOpen) {
|
||||
onMenuKeyDown(e)
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.key === 'Enter') {
|
||||
// Shift + Enter when multiline = newline
|
||||
const shouldAddNewline = e.shiftKey && multiline
|
||||
if (!shouldAddNewline) e.preventDefault(); // prevent newline from being created
|
||||
}
|
||||
onKeyDown?.(e)
|
||||
}, [onKeyDown, multiline])}
|
||||
}, [onKeyDown, onMenuKeyDown, multiline])}
|
||||
|
||||
rows={1}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
)
|
||||
{/* <div>{`idx ${optionIdx}`}</div> */}
|
||||
{isMenuOpen && (
|
||||
<div
|
||||
ref={refs.setFloating}
|
||||
className="z-[100] bg-void-bg-1 border-void-border-3 border rounded shadow-lg"
|
||||
style={{
|
||||
position: strategy,
|
||||
top: y ?? 0,
|
||||
left: x ?? 0,
|
||||
width: refs.reference.current instanceof HTMLElement ? refs.reference.current.offsetWidth : 0
|
||||
}}
|
||||
onWheel={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="py-1">
|
||||
{/* Path navigation breadcrumbs */}
|
||||
<div className="px-2 py-1 text-void-fg-3 text-sm border-b border-void-border-3">
|
||||
{[...path, newPathText].join(' > ')}
|
||||
</div>
|
||||
|
||||
{/* Options list */}
|
||||
{options.length === 0 ? (
|
||||
<div className="px-3 py-2 text-void-fg-3">No options available</div>
|
||||
) : (
|
||||
options.map((o, oIdx) => (
|
||||
<div
|
||||
ref={oIdx === optionIdx ? selectedOptionRef : null}
|
||||
|
||||
key={o.name}
|
||||
className={`px-3 py-1.5 cursor-pointer bg-void-bg-2 ${oIdx === optionIdx ? 'bg-void-bg-2-hover' : ''}`}
|
||||
onClick={() => { onSelectOption(); }}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<span className="text-void-fg-1">{o.displayName}</span>
|
||||
{o.nextOptions || o.generateNextOptions ? (
|
||||
<svg className="ml-2 h-3 w-3 text-void-fg-3" viewBox="0 0 12 12" fill="none">
|
||||
<path
|
||||
d="M4.5 2.5L8 6L4.5 9.5"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
})
|
||||
|
||||
|
||||
export const VoidSimpleInputBox = ({ value, onChangeValue, placeholder, className, disabled, passwordBlur, compact, ...inputProps }: {
|
||||
value: string;
|
||||
onChangeValue: (value: string) => void;
|
||||
placeholder: string;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
compact?: boolean;
|
||||
passwordBlur?: boolean;
|
||||
} & React.InputHTMLAttributes<HTMLInputElement>) => {
|
||||
|
||||
return (
|
||||
<input
|
||||
value={value}
|
||||
onChange={(e) => onChangeValue(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
// className='max-w-44 w-full border border-void-border-2 bg-void-bg-1 text-void-fg-3 text-root'
|
||||
// className={`w-full resize-none text-void-fg-1 placeholder:text-void-fg-3 px-2 py-1 rounded-sm
|
||||
className={`w-full resize-none bg-void-bg-1 text-void-fg-1 placeholder:text-void-fg-3 border border-void-border-2 focus:border-void-border-1
|
||||
${compact ? 'py-1 px-2' : 'py-2 px-4 '}
|
||||
rounded
|
||||
${disabled ? 'opacity-50 cursor-not-allowed' : ''}
|
||||
${className}`}
|
||||
style={{
|
||||
...passwordBlur && { WebkitTextSecurity: 'disc' },
|
||||
background: asCssVariable(inputBackground),
|
||||
color: asCssVariable(inputForeground)
|
||||
}}
|
||||
{...inputProps}
|
||||
type={undefined} // VS Code is doing some annoyingness that breaks paste if this is defined
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, placeholder, isPasswordField, multiline }: {
|
||||
onChangeText: (value: string) => void;
|
||||
styles?: Partial<IInputBoxStyles>,
|
||||
@@ -319,7 +725,7 @@ export const VoidSlider = ({
|
||||
size === 'xs' ? 'h-1' :
|
||||
size === 'sm' ? 'h-1.5' :
|
||||
size === 'sm+' ? 'h-2' : 'h-2.5'
|
||||
} bg-gray-200 dark:bg-gray-700 rounded-full cursor-pointer`}
|
||||
} bg-void-bg-2 rounded-full cursor-pointer`}
|
||||
onClick={handleTrackClick}
|
||||
>
|
||||
{/* Filled part of track */}
|
||||
@@ -328,12 +734,12 @@ export const VoidSlider = ({
|
||||
size === 'xs' ? 'h-1' :
|
||||
size === 'sm' ? 'h-1.5' :
|
||||
size === 'sm+' ? 'h-2' : 'h-2.5'
|
||||
} bg-gray-900 dark:bg-white rounded-full`}
|
||||
} bg-void-fg-1 rounded-full`}
|
||||
style={{ width: `${percentage}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Thumb with sizes matching VoidSwitch */}
|
||||
{/* Thumb */}
|
||||
<div
|
||||
className={`absolute top-1/2 transform -translate-x-1/2 -translate-y-1/2
|
||||
${size === 'xxs' ? 'h-2 w-2' :
|
||||
@@ -341,7 +747,8 @@ export const VoidSlider = ({
|
||||
size === 'sm' ? 'h-3 w-3' :
|
||||
size === 'sm+' ? 'h-3.5 w-3.5' : 'h-4 w-4'
|
||||
}
|
||||
bg-white dark:bg-gray-900 rounded-full shadow-md ${disabled ? 'cursor-not-allowed' : 'cursor-grab active:cursor-grabbing'}`}
|
||||
bg-void-fg-1 rounded-full shadow-md ${disabled ? 'cursor-not-allowed' : 'cursor-grab active:cursor-grabbing'}
|
||||
border border-void-fg-1`}
|
||||
style={{ left: `${percentage}%`, zIndex: 2 }} // Ensure thumb is above the invisible clickable area
|
||||
onMouseDown={(e) => {
|
||||
if (disabled) return;
|
||||
@@ -380,6 +787,7 @@ export const VoidSwitch = ({
|
||||
onChange,
|
||||
size = 'md',
|
||||
disabled = false,
|
||||
...props
|
||||
}: {
|
||||
value: boolean;
|
||||
onChange: (value: boolean) => void;
|
||||
@@ -387,13 +795,13 @@ export const VoidSwitch = ({
|
||||
size?: 'xxs' | 'xs' | 'sm' | 'sm+' | 'md';
|
||||
}) => {
|
||||
return (
|
||||
<label className="inline-flex items-center">
|
||||
<label className="inline-flex items-center" {...props}>
|
||||
<div
|
||||
onClick={() => !disabled && onChange(!value)}
|
||||
className={`
|
||||
cursor-pointer
|
||||
relative inline-flex items-center rounded-full transition-colors duration-200 ease-in-out
|
||||
${value ? 'bg-gray-900 dark:bg-white' : 'bg-gray-200 dark:bg-gray-700'}
|
||||
${value ? 'bg-zinc-900 dark:bg-white' : 'bg-white dark:bg-zinc-600'}
|
||||
${disabled ? 'opacity-25' : ''}
|
||||
${size === 'xxs' ? 'h-3 w-5' : ''}
|
||||
${size === 'xs' ? 'h-4 w-7' : ''}
|
||||
@@ -404,7 +812,7 @@ export const VoidSwitch = ({
|
||||
>
|
||||
<span
|
||||
className={`
|
||||
inline-block transform rounded-full bg-white dark:bg-gray-900 shadow transition-transform duration-200 ease-in-out
|
||||
inline-block transform rounded-full bg-white dark:bg-zinc-900 shadow transition-transform duration-200 ease-in-out
|
||||
${size === 'xxs' ? 'h-2 w-2' : ''}
|
||||
${size === 'xs' ? 'h-2.5 w-2.5' : ''}
|
||||
${size === 'sm' ? 'h-3 w-3' : ''}
|
||||
@@ -603,7 +1011,7 @@ export const VoidCustomDropdownBox = <T extends NonNullable<any>>({
|
||||
className="flex items-center h-4 bg-transparent whitespace-nowrap hover:brightness-90 w-full"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
<span className={`max-w-[120px] truncate ${arrowTouchesText ? 'mr-1' : ''}`}>
|
||||
<span className={`truncate ${arrowTouchesText ? 'mr-1' : ''}`}>
|
||||
{getOptionDisplayName(selectedOption)}
|
||||
</span>
|
||||
<svg
|
||||
@@ -625,57 +1033,60 @@ export const VoidCustomDropdownBox = <T extends NonNullable<any>>({
|
||||
{isOpen && (
|
||||
<div
|
||||
ref={refs.setFloating}
|
||||
className="z-10 bg-void-bg-1 border-void-border-1 border overflow-hidden rounded shadow-lg"
|
||||
className="z-[100] bg-void-bg-1 border-void-border-3 border rounded shadow-lg"
|
||||
style={{
|
||||
position: strategy,
|
||||
top: y ?? 0,
|
||||
left: x ?? 0,
|
||||
width: matchInputWidth
|
||||
width: (matchInputWidth
|
||||
? (refs.reference.current instanceof HTMLElement ? refs.reference.current.offsetWidth : 0)
|
||||
: Math.max(
|
||||
(refs.reference.current instanceof HTMLElement ? refs.reference.current.offsetWidth : 0),
|
||||
(measureRef.current instanceof HTMLElement ? measureRef.current.offsetWidth : 0)
|
||||
),
|
||||
))
|
||||
}}
|
||||
>
|
||||
{options.map((option) => {
|
||||
const thisOptionIsSelected = getOptionsEqual(option, selectedOption);
|
||||
const optionName = getOptionDropdownName(option);
|
||||
const optionDetail = getOptionDropdownDetail?.(option) || '';
|
||||
onWheel={(e) => e.stopPropagation()}
|
||||
><div className='overflow-auto max-h-80'>
|
||||
|
||||
return (
|
||||
<div
|
||||
key={optionName}
|
||||
className={`flex items-center px-2 py-1 cursor-pointer whitespace-nowrap
|
||||
{options.map((option) => {
|
||||
const thisOptionIsSelected = getOptionsEqual(option, selectedOption);
|
||||
const optionName = getOptionDropdownName(option);
|
||||
const optionDetail = getOptionDropdownDetail?.(option) || '';
|
||||
|
||||
return (
|
||||
<div
|
||||
key={optionName}
|
||||
className={`flex items-center px-2 py-1 pr-4 cursor-pointer whitespace-nowrap
|
||||
transition-all duration-100
|
||||
bg-void-bg-1
|
||||
${thisOptionIsSelected ? 'bg-void-bg-2' : 'hover:bg-void-bg-2'}
|
||||
${thisOptionIsSelected ? 'bg-void-bg-2-hover' : 'bg-void-bg-2-alt hover:bg-void-bg-2-hover'}
|
||||
`}
|
||||
onClick={() => {
|
||||
onChangeOption(option);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
<div className="w-4 flex justify-center flex-shrink-0">
|
||||
{thisOptionIsSelected && (
|
||||
<svg className="size-3" viewBox="0 0 12 12" fill="none">
|
||||
<path
|
||||
d="M10 3L4.5 8.5L2 6"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
onClick={() => {
|
||||
onChangeOption(option);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
<div className="w-4 flex justify-center flex-shrink-0">
|
||||
{thisOptionIsSelected && (
|
||||
<svg className="size-3" viewBox="0 0 12 12" fill="none">
|
||||
<path
|
||||
d="M10 3L4.5 8.5L2 6"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
<span className="flex justify-between items-center w-full gap-x-1">
|
||||
<span>{optionName}</span>
|
||||
<span className='text-void-fg-4 opacity-60'>{optionDetail}</span>
|
||||
</span>
|
||||
</div>
|
||||
<span className="flex justify-between w-full">
|
||||
<span>{optionName}</span>
|
||||
<span className='text-void-fg-4 opacity-60'>{optionDetail}</span>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -922,9 +1333,9 @@ export const BlockCode = ({ initValue, language, maxHeight, showScrollbars }: Bl
|
||||
}
|
||||
|
||||
|
||||
export const VoidButton = ({ children, disabled, onClick }: { children: React.ReactNode; disabled?: boolean; onClick: () => void }) => {
|
||||
export const VoidButtonBgDarken = ({ children, disabled, onClick, className }: { children: React.ReactNode; disabled?: boolean; onClick: () => void; className?: string }) => {
|
||||
return <button disabled={disabled}
|
||||
className='px-3 py-1 bg-black/10 dark:bg-gray-200/10 rounded-sm overflow-hidden whitespace-nowrap'
|
||||
className={`px-3 py-1 bg-black/10 dark:bg-white/10 rounded-sm overflow-hidden whitespace-nowrap flex items-center justify-center ${className || ''}`}
|
||||
onClick={onClick}
|
||||
>{children}</button>
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ import { IThemeService } from '../../../../../../../platform/theme/common/themeS
|
||||
import { ILLMMessageService } from '../../../../common/sendLLMMessageService.js';
|
||||
import { IRefreshModelService } from '../../../../../../../workbench/contrib/void/common/refreshModelService.js';
|
||||
import { IVoidSettingsService } from '../../../../../../../workbench/contrib/void/common/voidSettingsService.js';
|
||||
import { IEditCodeService } from '../../../editCodeServiceInterface.js'
|
||||
|
||||
import { ISidebarStateService } from '../../../sidebarStateService.js';
|
||||
import { IInstantiationService } from '../../../../../../../platform/instantiation/common/instantiation.js'
|
||||
@@ -46,6 +45,10 @@ import { ILanguageService } from '../../../../../../../editor/common/languages/l
|
||||
import { IVoidModelService } from '../../../../common/voidModelService.js'
|
||||
import { IWorkspaceContextService } from '../../../../../../../platform/workspace/common/workspace.js'
|
||||
import { IVoidCommandBarService } from '../../../voidCommandBarService.js'
|
||||
import { INativeHostService } from '../../../../../../../platform/native/common/native.js';
|
||||
import { IEditCodeService } from '../../../editCodeServiceInterface.js'
|
||||
import { IToolsService } from '../../../toolsService.js'
|
||||
import { IConvertToLLMMessageService } from '../../../convertToLLMMessageService.js'
|
||||
|
||||
|
||||
// normally to do this you'd use a useEffect that calls .onDidChangeState(), but useEffect mounts too late and misses initial state changes
|
||||
@@ -213,6 +216,9 @@ const getReactAccessor = (accessor: ServicesAccessor) => {
|
||||
IWorkspaceContextService: accessor.get(IWorkspaceContextService),
|
||||
|
||||
IVoidCommandBarService: accessor.get(IVoidCommandBarService),
|
||||
INativeHostService: accessor.get(INativeHostService),
|
||||
IToolsService: accessor.get(IToolsService),
|
||||
IConvertToLLMMessageService: accessor.get(IConvertToLLMMessageService),
|
||||
|
||||
} as const
|
||||
return reactAccessor
|
||||
@@ -298,6 +304,16 @@ export const useChatThreadsStreamState = (threadId: string) => {
|
||||
return s
|
||||
}
|
||||
|
||||
export const useFullChatThreadsStreamState = () => {
|
||||
const [s, ss] = useState(chatThreadsStreamState)
|
||||
useEffect(() => {
|
||||
ss(chatThreadsStreamState)
|
||||
const listener = () => { ss(chatThreadsStreamState) }
|
||||
chatThreadsStreamStateListeners.add(listener)
|
||||
return () => { chatThreadsStreamStateListeners.delete(listener) }
|
||||
}, [ss])
|
||||
return s
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -348,9 +364,9 @@ export const useCommandBarURIListener = (listener: (uri: URI) => void) => {
|
||||
export const useCommandBarState = () => {
|
||||
const accessor = useAccessor()
|
||||
const commandBarService = accessor.get('IVoidCommandBarService')
|
||||
const [s, ss] = useState({ state: commandBarService.stateOfURI, sortedURIs: commandBarService.sortedURIs });
|
||||
const [s, ss] = useState({ stateOfURI: commandBarService.stateOfURI, sortedURIs: commandBarService.sortedURIs });
|
||||
const listener = useCallback(() => {
|
||||
ss({ state: commandBarService.stateOfURI, sortedURIs: commandBarService.sortedURIs });
|
||||
ss({ stateOfURI: commandBarService.stateOfURI, sortedURIs: commandBarService.sortedURIs });
|
||||
}, [commandBarService])
|
||||
useCommandBarURIListener(listener)
|
||||
|
||||
|
||||
@@ -1,128 +1,130 @@
|
||||
import { useEffect } from 'react';
|
||||
// Get rid of this as it was causing lag
|
||||
|
||||
export const useScrollbarStyles = (containerRef: React.MutableRefObject<HTMLDivElement | null>) => {
|
||||
useEffect(() => {
|
||||
if (!containerRef.current) return;
|
||||
// import { useEffect } from 'react';
|
||||
|
||||
// Create selector for specific overflow classes
|
||||
const overflowSelector = [
|
||||
'[class*="overflow-auto"]',
|
||||
'[class*="overflow-x-auto"]',
|
||||
'[class*="overflow-y-auto"]'
|
||||
].join(',');
|
||||
// export const useScrollbarStyles = (containerRef: React.RefObject<HTMLDivElement | null>) => {
|
||||
// useEffect(() => {
|
||||
// if (!containerRef.current) return;
|
||||
|
||||
// Function to initialize scrollbar styles for elements
|
||||
const initializeScrollbarStyles = () => {
|
||||
// Get all matching elements within the container, including the container itself
|
||||
const scrollElements = [
|
||||
...(containerRef.current?.matches(overflowSelector) ? [containerRef.current] : []),
|
||||
...Array.from(containerRef.current?.querySelectorAll(overflowSelector) || [])
|
||||
];
|
||||
// // Create selector for specific overflow classes
|
||||
// const overflowSelector = [
|
||||
// '[class*="overflow-auto"]',
|
||||
// '[class*="overflow-x-auto"]',
|
||||
// '[class*="overflow-y-auto"]'
|
||||
// ].join(',');
|
||||
|
||||
// Apply basic styling to all elements
|
||||
scrollElements.forEach(element => {
|
||||
element.classList.add('void-scrollable-element');
|
||||
});
|
||||
// // Function to initialize scrollbar styles for elements
|
||||
// const initializeScrollbarStyles = () => {
|
||||
// // Get all matching elements within the container, including the container itself
|
||||
// const scrollElements = [
|
||||
// ...(containerRef.current?.matches(overflowSelector) ? [containerRef.current] : []),
|
||||
// ...Array.from(containerRef.current?.querySelectorAll(overflowSelector) || [])
|
||||
// ];
|
||||
|
||||
// Only initialize fade effects for elements that haven't been initialized yet
|
||||
scrollElements.forEach(element => {
|
||||
if (!(element as any).__scrollbarCleanup) {
|
||||
let fadeTimeout: NodeJS.Timeout | null = null;
|
||||
let fadeInterval: NodeJS.Timeout | null = null;
|
||||
// // Apply basic styling to all elements
|
||||
// scrollElements.forEach(element => {
|
||||
// element.classList.add('void-scrollable-element');
|
||||
// });
|
||||
|
||||
const fadeIn = () => {
|
||||
if (fadeInterval) clearInterval(fadeInterval);
|
||||
// // Only initialize fade effects for elements that haven't been initialized yet
|
||||
// scrollElements.forEach(element => {
|
||||
// if (!(element as any).__scrollbarCleanup) {
|
||||
// let fadeTimeout: NodeJS.Timeout | null = null;
|
||||
// let fadeInterval: NodeJS.Timeout | null = null;
|
||||
|
||||
let step = 0;
|
||||
fadeInterval = setInterval(() => {
|
||||
if (step <= 10) {
|
||||
element.classList.remove(`show-scrollbar-${step - 1}`);
|
||||
element.classList.add(`show-scrollbar-${step}`);
|
||||
step++;
|
||||
} else {
|
||||
clearInterval(fadeInterval!);
|
||||
}
|
||||
}, 10);
|
||||
};
|
||||
// const fadeIn = () => {
|
||||
// if (fadeInterval) clearInterval(fadeInterval);
|
||||
|
||||
const fadeOut = () => {
|
||||
if (fadeInterval) clearInterval(fadeInterval);
|
||||
// let step = 0;
|
||||
// fadeInterval = setInterval(() => {
|
||||
// if (step <= 10) {
|
||||
// element.classList.remove(`show-scrollbar-${step - 1}`);
|
||||
// element.classList.add(`show-scrollbar-${step}`);
|
||||
// step++;
|
||||
// } else {
|
||||
// clearInterval(fadeInterval!);
|
||||
// }
|
||||
// }, 10);
|
||||
// };
|
||||
|
||||
let step = 10;
|
||||
fadeInterval = setInterval(() => {
|
||||
if (step >= 0) {
|
||||
element.classList.remove(`show-scrollbar-${step + 1}`);
|
||||
element.classList.add(`show-scrollbar-${step}`);
|
||||
step--;
|
||||
} else {
|
||||
clearInterval(fadeInterval!);
|
||||
}
|
||||
}, 60);
|
||||
};
|
||||
// const fadeOut = () => {
|
||||
// if (fadeInterval) clearInterval(fadeInterval);
|
||||
|
||||
const onMouseEnter = () => {
|
||||
if (fadeTimeout) clearTimeout(fadeTimeout);
|
||||
if (fadeInterval) clearInterval(fadeInterval);
|
||||
fadeIn();
|
||||
};
|
||||
// let step = 10;
|
||||
// fadeInterval = setInterval(() => {
|
||||
// if (step >= 0) {
|
||||
// element.classList.remove(`show-scrollbar-${step + 1}`);
|
||||
// element.classList.add(`show-scrollbar-${step}`);
|
||||
// step--;
|
||||
// } else {
|
||||
// clearInterval(fadeInterval!);
|
||||
// }
|
||||
// }, 60);
|
||||
// };
|
||||
|
||||
const onMouseLeave = () => {
|
||||
if (fadeTimeout) clearTimeout(fadeTimeout);
|
||||
fadeTimeout = setTimeout(() => {
|
||||
fadeOut();
|
||||
}, 10);
|
||||
};
|
||||
// const onMouseEnter = () => {
|
||||
// if (fadeTimeout) clearTimeout(fadeTimeout);
|
||||
// if (fadeInterval) clearInterval(fadeInterval);
|
||||
// fadeIn();
|
||||
// };
|
||||
|
||||
element.addEventListener('mouseenter', onMouseEnter);
|
||||
element.addEventListener('mouseleave', onMouseLeave);
|
||||
// const onMouseLeave = () => {
|
||||
// if (fadeTimeout) clearTimeout(fadeTimeout);
|
||||
// fadeTimeout = setTimeout(() => {
|
||||
// fadeOut();
|
||||
// }, 10);
|
||||
// };
|
||||
|
||||
// Store cleanup function
|
||||
const cleanup = () => {
|
||||
element.removeEventListener('mouseenter', onMouseEnter);
|
||||
element.removeEventListener('mouseleave', onMouseLeave);
|
||||
if (fadeTimeout) clearTimeout(fadeTimeout);
|
||||
if (fadeInterval) clearInterval(fadeInterval);
|
||||
element.classList.remove('void-scrollable-element');
|
||||
// Remove any remaining show-scrollbar classes
|
||||
for (let i = 0; i <= 10; i++) {
|
||||
element.classList.remove(`show-scrollbar-${i}`);
|
||||
}
|
||||
};
|
||||
// element.addEventListener('mouseenter', onMouseEnter);
|
||||
// element.addEventListener('mouseleave', onMouseLeave);
|
||||
|
||||
// Store the cleanup function on the element for later use
|
||||
(element as any).__scrollbarCleanup = cleanup;
|
||||
}
|
||||
});
|
||||
};
|
||||
// // Store cleanup function
|
||||
// const cleanup = () => {
|
||||
// element.removeEventListener('mouseenter', onMouseEnter);
|
||||
// element.removeEventListener('mouseleave', onMouseLeave);
|
||||
// if (fadeTimeout) clearTimeout(fadeTimeout);
|
||||
// if (fadeInterval) clearInterval(fadeInterval);
|
||||
// element.classList.remove('void-scrollable-element');
|
||||
// // Remove any remaining show-scrollbar classes
|
||||
// for (let i = 0; i <= 10; i++) {
|
||||
// element.classList.remove(`show-scrollbar-${i}`);
|
||||
// }
|
||||
// };
|
||||
|
||||
// Initialize for the first time
|
||||
initializeScrollbarStyles();
|
||||
// // Store the cleanup function on the element for later use
|
||||
// (element as any).__scrollbarCleanup = cleanup;
|
||||
// }
|
||||
// });
|
||||
// };
|
||||
|
||||
// Set up mutation observer to do the same
|
||||
const observer = new MutationObserver(() => {
|
||||
initializeScrollbarStyles();
|
||||
});
|
||||
// // Initialize for the first time
|
||||
// initializeScrollbarStyles();
|
||||
|
||||
// Start observing the container for child changes
|
||||
observer.observe(containerRef.current, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
// // Set up mutation observer to do the same
|
||||
// const observer = new MutationObserver(() => {
|
||||
// initializeScrollbarStyles();
|
||||
// });
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
// Your existing cleanup code...
|
||||
if (containerRef.current) {
|
||||
const scrollElements = [
|
||||
...(containerRef.current.matches(overflowSelector) ? [containerRef.current] : []),
|
||||
...Array.from(containerRef.current.querySelectorAll(overflowSelector))
|
||||
];
|
||||
scrollElements.forEach(element => {
|
||||
if ((element as any).__scrollbarCleanup) {
|
||||
(element as any).__scrollbarCleanup();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}, [containerRef]);
|
||||
};
|
||||
// // Start observing the container for child changes
|
||||
// observer.observe(containerRef.current, {
|
||||
// childList: true,
|
||||
// subtree: true
|
||||
// });
|
||||
|
||||
// return () => {
|
||||
// observer.disconnect();
|
||||
// // Your existing cleanup code...
|
||||
// if (containerRef.current) {
|
||||
// const scrollElements = [
|
||||
// ...(containerRef.current.matches(overflowSelector) ? [containerRef.current] : []),
|
||||
// ...Array.from(containerRef.current.querySelectorAll(overflowSelector))
|
||||
// ];
|
||||
// scrollElements.forEach(element => {
|
||||
// if ((element as any).__scrollbarCleanup) {
|
||||
// (element as any).__scrollbarCleanup();
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
// };
|
||||
// }, [containerRef]);
|
||||
// };
|
||||
|
||||
+47
-37
@@ -9,8 +9,9 @@ import { useAccessor, useCommandBarState, useIsDark } from '../util/services.js'
|
||||
import '../styles.css'
|
||||
import { useCallback, useEffect, useState, useRef } from 'react';
|
||||
import { ScrollType } from '../../../../../../../editor/common/editorCommon.js';
|
||||
import { acceptAllBg, acceptBorder, buttonFontSize, buttonTextColor, rejectAllBg, rejectBorder } from '../../../../common/helpers/colors.js';
|
||||
import { acceptAllBg, acceptBorder, buttonFontSize, buttonTextColor, rejectBg, rejectBorder } from '../../../../common/helpers/colors.js';
|
||||
import { VoidCommandBarProps } from '../../../voidCommandBarService.js';
|
||||
import { AcceptAllButtonWrapper, RejectAllButtonWrapper } from '../sidebar-tsx/SidebarChat.js';
|
||||
|
||||
export const VoidCommandBarMain = ({ uri, editor }: VoidCommandBarProps) => {
|
||||
const isDark = useIsDark()
|
||||
@@ -39,7 +40,7 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => {
|
||||
const commandService = accessor.get('ICommandService')
|
||||
const commandBarService = accessor.get('IVoidCommandBarService')
|
||||
const voidModelService = accessor.get('IVoidModelService')
|
||||
const { state: commandBarState, sortedURIs: sortedCommandBarURIs } = useCommandBarState()
|
||||
const { stateOfURI: commandBarState, sortedURIs: sortedCommandBarURIs } = useCommandBarState()
|
||||
|
||||
|
||||
// useEffect(() => {
|
||||
@@ -97,7 +98,7 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => {
|
||||
if (diffid === undefined) return
|
||||
const diff = editCodeService.diffOfId[diffid]
|
||||
if (!diff) return
|
||||
editor.revealLineNearTop(diff.startLine, ScrollType.Immediate)
|
||||
editor.revealLineNearTop(diff.startLine - 1, ScrollType.Immediate)
|
||||
commandBarService.setDiffIdx(uri, idx)
|
||||
}
|
||||
const getNextUriIdx = (step: 1 | -1) => {
|
||||
@@ -110,7 +111,7 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => {
|
||||
const { model } = await voidModelService.getModelSafe(nextURI)
|
||||
if (model) {
|
||||
// switch to the URI
|
||||
editorService.openCodeEditor({ resource: nextURI, options: { revealIfVisible: true } }, editor)
|
||||
editorService.openCodeEditor({ resource: model.uri, options: { revealIfVisible: true } }, editor)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,38 +212,47 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => {
|
||||
|
||||
if (!isADiffZoneInAnyFile) return null
|
||||
|
||||
const acceptAllButton = <button
|
||||
className='text-nowrap'
|
||||
// const acceptAllButton = <button
|
||||
// className='text-nowrap'
|
||||
// onClick={onAcceptAll}
|
||||
// style={{
|
||||
// backgroundColor: acceptAllBg,
|
||||
// border: acceptBorder,
|
||||
// color: buttonTextColor,
|
||||
// fontSize: buttonFontSize,
|
||||
// padding: '2px 4px',
|
||||
// borderRadius: '6px',
|
||||
// cursor: 'pointer'
|
||||
// }}
|
||||
// >
|
||||
// Accept File
|
||||
// </button>
|
||||
|
||||
// const rejectAllButton = <button
|
||||
// className='text-nowrap'
|
||||
// onClick={onRejectAll}
|
||||
// style={{
|
||||
// backgroundColor: rejectBg,
|
||||
// border: rejectBorder,
|
||||
// color: 'white',
|
||||
// fontSize: buttonFontSize,
|
||||
// padding: '2px 4px',
|
||||
// borderRadius: '6px',
|
||||
// cursor: 'pointer'
|
||||
// }}
|
||||
// >
|
||||
// Reject File
|
||||
// </button>
|
||||
|
||||
const acceptAllButton = <AcceptAllButtonWrapper
|
||||
text={'Keep Changes'}
|
||||
onClick={onAcceptAll}
|
||||
style={{
|
||||
backgroundColor: acceptAllBg,
|
||||
border: acceptBorder,
|
||||
color: buttonTextColor,
|
||||
fontSize: buttonFontSize,
|
||||
padding: '2px 4px',
|
||||
borderRadius: '6px',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
Accept File
|
||||
</button>
|
||||
/>
|
||||
|
||||
|
||||
const rejectAllButton = <button
|
||||
className='text-nowrap'
|
||||
const rejectAllButton = <RejectAllButtonWrapper
|
||||
text={'Reject All'}
|
||||
onClick={onRejectAll}
|
||||
style={{
|
||||
backgroundColor: rejectAllBg,
|
||||
border: rejectBorder,
|
||||
color: 'white',
|
||||
fontSize: buttonFontSize,
|
||||
padding: '2px 4px',
|
||||
borderRadius: '6px',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
Reject File
|
||||
</button>
|
||||
/>
|
||||
|
||||
const acceptRejectAllButtons = <div className="flex items-center gap-1 text-sm">
|
||||
{acceptAllButton}
|
||||
@@ -271,9 +281,9 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => {
|
||||
<div className="flex flex-col gap-1">
|
||||
{/* Changes in file */}
|
||||
<div className={`${!isADiffZoneInThisFile ? 'hidden' : ''} flex items-center ${upDownDisabled ? 'opacity-50' : ''}`}>
|
||||
{downButton}
|
||||
{upButton}
|
||||
<span className="min-w-16 px-2 text-xs">
|
||||
{downButton}
|
||||
<span className="min-w-16 px-2 text-xs leading-[1]">
|
||||
{isADiffInThisFile ?
|
||||
`Diff ${(currDiffIdx ?? 0) + 1} of ${sortedDiffIds.length}`
|
||||
: streamState === 'streaming' ?
|
||||
@@ -289,7 +299,7 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => {
|
||||
{/* <div className="w-px h-3 bg-void-border-3 mx-0.5 shadow-sm"></div> */}
|
||||
{rightButton}
|
||||
{/* <div className="w-px h-3 bg-void-border-3 mx-0.5 shadow-sm"></div> */}
|
||||
<span className="min-w-16 px-2 text-xs">
|
||||
<span className="min-w-16 px-2 text-xs leading-[1]">
|
||||
{currFileIdx !== null ?
|
||||
`File ${currFileIdx + 1} of ${sortedCommandBarURIs.length}`
|
||||
: `${sortedCommandBarURIs.length} file${sortedCommandBarURIs.length === 1 ? '' : 's'} changed`
|
||||
@@ -299,7 +309,7 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
return <div className={`flex flex-col items-center gap-y-2 mx-2 pointer-events-auto`}>
|
||||
return <div className={`flex flex-col items-center gap-y-2 pointer-events-auto`}>
|
||||
{showAcceptRejectAll && acceptRejectAllButtons}
|
||||
{leftRightUpDownButtons}
|
||||
|
||||
+170
@@ -0,0 +1,170 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
|
||||
import { useAccessor, useActiveURI, useIsDark, useSettingsState } from '../util/services.js';
|
||||
|
||||
import '../styles.css'
|
||||
import { VOID_CTRL_K_ACTION_ID, VOID_CTRL_L_ACTION_ID } from '../../../actionIDs.js';
|
||||
import { Circle, MoreVertical } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { VoidSelectionHelperProps } from '../../../../../../contrib/void/browser/voidSelectionHelperWidget.js';
|
||||
import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js';
|
||||
|
||||
|
||||
export const VoidSelectionHelperMain = (props: VoidSelectionHelperProps) => {
|
||||
|
||||
const isDark = useIsDark()
|
||||
|
||||
return <div
|
||||
className={`@@void-scope ${isDark ? 'dark' : ''}`}
|
||||
>
|
||||
<VoidSelectionHelper {...props} />
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
|
||||
const VoidSelectionHelper = ({ rerenderKey }: VoidSelectionHelperProps) => {
|
||||
|
||||
|
||||
const accessor = useAccessor()
|
||||
const keybindingService = accessor.get('IKeybindingService')
|
||||
const commandService = accessor.get('ICommandService')
|
||||
|
||||
const ctrlLKeybind = keybindingService.lookupKeybinding(VOID_CTRL_L_ACTION_ID)
|
||||
const ctrlKKeybind = keybindingService.lookupKeybinding(VOID_CTRL_K_ACTION_ID)
|
||||
|
||||
const dividerHTML = <div className='w-[0.5px] bg-void-border-3'></div>
|
||||
|
||||
const [reactRerenderCount, setReactRerenderKey] = useState(rerenderKey)
|
||||
const [clickState, setClickState] = useState<'init' | 'clickedOption' | 'clickedMore'>('init')
|
||||
|
||||
useEffect(() => {
|
||||
const disposable = commandService.onWillExecuteCommand(e => {
|
||||
if (e.commandId === VOID_CTRL_L_ACTION_ID || e.commandId === VOID_CTRL_K_ACTION_ID) {
|
||||
setClickState('clickedOption')
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
disposable.dispose();
|
||||
};
|
||||
}, [commandService, setClickState]);
|
||||
|
||||
|
||||
// rerender when the key changes
|
||||
if (reactRerenderCount !== rerenderKey) {
|
||||
setReactRerenderKey(rerenderKey)
|
||||
setClickState('init')
|
||||
}
|
||||
// useEffect(() => {
|
||||
// }, [rerenderKey, reactRerenderCount, setReactRerenderKey, setClickState])
|
||||
|
||||
// if the user selected an option, close
|
||||
|
||||
|
||||
if (clickState === 'clickedOption') {
|
||||
return null
|
||||
}
|
||||
|
||||
const defaultHTML = <>
|
||||
{ctrlLKeybind &&
|
||||
<div
|
||||
className='
|
||||
flex items-center px-2 py-1.5
|
||||
cursor-pointer
|
||||
'
|
||||
onClick={() => {
|
||||
commandService.executeCommand(VOID_CTRL_L_ACTION_ID)
|
||||
setClickState('clickedOption');
|
||||
}}
|
||||
>
|
||||
<span>Add to Chat</span>
|
||||
<span className='ml-1 px-1 rounded bg-[var(--vscode-keybindingLabel-background)] text-[var(--vscode-keybindingLabel-foreground)] border border-[var(--vscode-keybindingLabel-border)]'>
|
||||
{ctrlLKeybind.getLabel()}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
{ctrlLKeybind && ctrlKKeybind &&
|
||||
dividerHTML
|
||||
}
|
||||
{ctrlKKeybind &&
|
||||
<div
|
||||
className='
|
||||
flex items-center px-2 py-1.5
|
||||
cursor-pointer
|
||||
'
|
||||
onClick={() => {
|
||||
commandService.executeCommand(VOID_CTRL_K_ACTION_ID)
|
||||
setClickState('clickedOption');
|
||||
}}
|
||||
>
|
||||
<span className='ml-1'>Edit Inline</span>
|
||||
<span className='ml-1 px-1 rounded bg-[var(--vscode-keybindingLabel-background)] text-[var(--vscode-keybindingLabel-foreground)] border border-[var(--vscode-keybindingLabel-border)]'>
|
||||
{ctrlKKeybind.getLabel()}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
{dividerHTML}
|
||||
|
||||
<div
|
||||
className='
|
||||
flex items-center px-0.5
|
||||
cursor-pointer
|
||||
'
|
||||
onClick={() => {
|
||||
setClickState('clickedMore');
|
||||
}}
|
||||
>
|
||||
<MoreVertical className="w-4" />
|
||||
</div>
|
||||
</>
|
||||
|
||||
|
||||
const moreOptionsHTML = <>
|
||||
<div
|
||||
className='
|
||||
flex items-center px-2 py-1.5
|
||||
cursor-pointer
|
||||
'
|
||||
onClick={() => {
|
||||
commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID);
|
||||
setClickState('clickedOption');
|
||||
}}
|
||||
>
|
||||
Disable Suggestions?
|
||||
</div>
|
||||
|
||||
{dividerHTML}
|
||||
|
||||
<div
|
||||
className='
|
||||
flex items-center px-0.5
|
||||
cursor-pointer
|
||||
'
|
||||
onClick={() => {
|
||||
setClickState('init');
|
||||
}}
|
||||
>
|
||||
<MoreVertical className="w-4" />
|
||||
</div>
|
||||
</>
|
||||
|
||||
return <div className='
|
||||
pointer-events-auto select-none
|
||||
z-[1000]
|
||||
rounded-sm shadow-md flex flex-nowrap text-nowrap
|
||||
border border-void-border-3 bg-void-bg-2
|
||||
transition-all duration-200
|
||||
'>
|
||||
{clickState === 'init' ? defaultHTML
|
||||
: clickState === 'clickedMore' ? moreOptionsHTML
|
||||
: <></>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
+4
@@ -5,5 +5,9 @@
|
||||
|
||||
import { mountFnGenerator } from '../util/mountFnGenerator.js'
|
||||
import { VoidCommandBarMain } from './VoidCommandBar.js'
|
||||
import { VoidSelectionHelperMain } from './VoidSelectionHelper.js'
|
||||
|
||||
export const mountVoidCommandBar = mountFnGenerator(VoidCommandBarMain)
|
||||
|
||||
export const mountVoidSelectionHelper = mountFnGenerator(VoidSelectionHelperMain)
|
||||
|
||||
@@ -0,0 +1,903 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useAccessor, useIsDark, useSettingsState } from '../util/services.js';
|
||||
import { Brain, Check, ChevronRight, DollarSign, ExternalLink, Lock, X } from 'lucide-react';
|
||||
import { displayInfoOfProviderName, ProviderName, providerNames, refreshableProviderNames } from '../../../../common/voidSettingsTypes.js';
|
||||
import { getModelCapabilities, ollamaRecommendedModels } from '../../../../common/modelCapabilities.js';
|
||||
import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js';
|
||||
import { AddModelInputBox, AnimatedCheckmarkButton, OllamaSetupInstructions, OneClickSwitchButton, SettingsForProvider } from '../void-settings-tsx/Settings.js';
|
||||
import { ColorScheme } from '../../../../../../../platform/theme/common/theme.js';
|
||||
import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js';
|
||||
|
||||
const OVERRIDE_VALUE = false
|
||||
|
||||
export const VoidOnboarding = () => {
|
||||
|
||||
const voidSettingsState = useSettingsState()
|
||||
const isOnboardingComplete = voidSettingsState.globalSettings.isOnboardingComplete || OVERRIDE_VALUE
|
||||
|
||||
const isDark = useIsDark()
|
||||
|
||||
return (
|
||||
<div className={`@@void-scope ${isDark ? 'dark' : ''}`}>
|
||||
<div
|
||||
className={`
|
||||
bg-void-bg-3 fixed top-0 right-0 bottom-0 left-0 width-full h-full z-[99999]
|
||||
transition-all duration-1000 ${isOnboardingComplete ? 'opacity-0 pointer-events-none' : 'opacity-100 pointer-events-auto'}
|
||||
`}
|
||||
>
|
||||
<ErrorBoundary>
|
||||
<VoidOnboardingContent />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const VoidIcon = () => {
|
||||
const accessor = useAccessor()
|
||||
const themeService = accessor.get('IThemeService')
|
||||
|
||||
const divRef = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
// void icon style
|
||||
const updateTheme = () => {
|
||||
const theme = themeService.getColorTheme().type
|
||||
const isDark = theme === ColorScheme.DARK || theme === ColorScheme.HIGH_CONTRAST_DARK
|
||||
if (divRef.current) {
|
||||
divRef.current.style.maxWidth = '220px'
|
||||
divRef.current.style.opacity = '50%'
|
||||
divRef.current.style.filter = isDark ? '' : 'invert(1)' //brightness(.5)
|
||||
}
|
||||
}
|
||||
updateTheme()
|
||||
const d = themeService.onDidColorThemeChange(updateTheme)
|
||||
return () => d.dispose()
|
||||
}, [])
|
||||
|
||||
return <div ref={divRef} className='@@void-void-icon' />
|
||||
}
|
||||
|
||||
const FADE_DURATION_MS = 2000
|
||||
|
||||
const FadeIn = ({ children, className, delayMs = 0, durationMs, ...props }: { children: React.ReactNode, delayMs?: number, durationMs?: number, className?: string } & React.HTMLAttributes<HTMLDivElement>) => {
|
||||
|
||||
const [opacity, setOpacity] = useState(0)
|
||||
|
||||
const effectiveDurationMs = durationMs ?? FADE_DURATION_MS
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
setOpacity(1)
|
||||
}, delayMs)
|
||||
|
||||
return () => clearTimeout(timeout)
|
||||
}, [setOpacity, delayMs])
|
||||
|
||||
|
||||
return (
|
||||
<div className={className} style={{ opacity, transition: `opacity ${effectiveDurationMs}ms ease-in-out` }} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Onboarding
|
||||
// OnboardingPage
|
||||
// title:
|
||||
// div
|
||||
// "Welcome to Void"
|
||||
// image
|
||||
// content:<></>
|
||||
// title
|
||||
// content
|
||||
// prev/next
|
||||
|
||||
// OnboardingPage
|
||||
// title:
|
||||
// div
|
||||
// "How would you like to use Void?"
|
||||
// content:
|
||||
// ModelQuestionContent
|
||||
// |
|
||||
// div
|
||||
// "I want to:"
|
||||
// div
|
||||
// "Use the smartest models"
|
||||
// "Keep my data fully private"
|
||||
// "Save money"
|
||||
// "I don't know"
|
||||
// | div
|
||||
// | div
|
||||
// "We recommend using "
|
||||
// "Set API"
|
||||
// | div
|
||||
// ""
|
||||
// | div
|
||||
//
|
||||
// title
|
||||
// content
|
||||
// prev/next
|
||||
//
|
||||
// OnboardingPage
|
||||
// title
|
||||
// content
|
||||
// prev/next
|
||||
|
||||
const NextButton = ({ onClick, ...props }: { onClick: () => void } & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className="px-6 py-2 bg-zinc-100 enabled:hover:bg-zinc-100 disabled:bg-zinc-100/40 disabled:cursor-not-allowed rounded text-black duration-600 transition-all"
|
||||
{...props.disabled && {
|
||||
'data-tooltip-id': 'void-tooltip',
|
||||
'data-tooltip-content': 'Please enter all required fields or choose another provider',
|
||||
'data-tooltip-place': 'top',
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
const PreviousButton = ({ onClick, ...props }: { onClick: () => void } & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className="px-6 py-2 rounded text-void-fg-3 opacity-80 hover:brightness-115 duration-600 transition-all"
|
||||
{...props}
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const OnboardingPageShell = ({ top, bottom, content, hasMaxWidth = true, className = '', }: {
|
||||
top?: React.ReactNode,
|
||||
bottom?: React.ReactNode,
|
||||
content?: React.ReactNode,
|
||||
hasMaxWidth?: boolean,
|
||||
className?: string,
|
||||
}) => {
|
||||
return (
|
||||
<div className={`min-h-full text-lg flex flex-col gap-4 w-full mx-auto ${hasMaxWidth ? 'max-w-[600px]' : ''} ${className}`}>
|
||||
{top && <FadeIn className='w-full mb-auto pt-16'>{top}</FadeIn>}
|
||||
{content && <FadeIn className='w-full my-auto'>{content}</FadeIn>}
|
||||
{bottom && <div className='w-full pb-8'>{bottom}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const OllamaDownloadOrRemoveModelButton = ({ modelName, isModelInstalled, sizeGb }: { modelName: string, isModelInstalled: boolean, sizeGb: number | false | 'not-known' }) => {
|
||||
|
||||
|
||||
// for now just link to the ollama download page
|
||||
return <a
|
||||
href={`https://ollama.com/library/${modelName}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center text-void-fg-2 hover:text-void-fg-1"
|
||||
>
|
||||
<ExternalLink className="w-3.5 h-3.5" />
|
||||
</a>
|
||||
|
||||
// if (isModelInstalled) {
|
||||
// return <div className="flex items-center">
|
||||
|
||||
// <span className="flex items-center">Uninstall</span>
|
||||
|
||||
// <IconShell1
|
||||
// className="ml-1"
|
||||
// Icon={Trash}
|
||||
// onClick={() => {
|
||||
|
||||
// setIsModelInstalling(false);
|
||||
// }}
|
||||
// />
|
||||
|
||||
// </div>
|
||||
// }
|
||||
|
||||
|
||||
|
||||
// else if (isModelInstalling) {
|
||||
// return <div className="flex items-center">
|
||||
|
||||
// <span className="flex items-center">{`Download? ${typeof sizeGb === 'number' ? `(${sizeGb} Gb)` : ''}`}</span>
|
||||
|
||||
// <IconShell1
|
||||
// className="ml-1"
|
||||
// Icon={Square}
|
||||
// onClick={() => {
|
||||
// // abort()
|
||||
|
||||
// // TODO!!!!!!!!!!! don't do this
|
||||
// setIsModelInstalling(false);
|
||||
// }}
|
||||
// />
|
||||
|
||||
// </div>
|
||||
// }
|
||||
|
||||
|
||||
// else if (!isModelInstalled) {
|
||||
|
||||
// return <div className="flex items-center">
|
||||
|
||||
// <span className="flex items-center">Download ({sizeGb} Gb)</span>
|
||||
|
||||
// <IconShell1
|
||||
// className="ml-1"
|
||||
// Icon={Download}
|
||||
// onClick={() => {
|
||||
// // this is a check for whether the model was installed:
|
||||
|
||||
// if (isModelInstalling) return
|
||||
|
||||
|
||||
// // TODO!!!!!! don't do this
|
||||
|
||||
|
||||
// // install(modelname), callback = setIsModelInstalling(false);
|
||||
|
||||
// setIsModelInstalling(true);
|
||||
// }}
|
||||
// />
|
||||
|
||||
// </div>
|
||||
|
||||
// }
|
||||
|
||||
// return <></>
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
const YesNoText = ({ val }: { val: boolean | null }) => {
|
||||
|
||||
return <div
|
||||
className={
|
||||
val === true ? "text text-emerald-500"
|
||||
: val === false ? 'text-rose-600'
|
||||
: "text text-amber-300"
|
||||
}
|
||||
>
|
||||
{
|
||||
val === true ? "Yes"
|
||||
: val === false ? 'No'
|
||||
: "Yes*"
|
||||
}
|
||||
</div>
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
const abbreviateNumber = (num: number): string => {
|
||||
if (num >= 1000000) {
|
||||
// For millions
|
||||
return Math.floor(num / 1000000) + 'M';
|
||||
} else if (num >= 1000) {
|
||||
// For thousands
|
||||
return Math.floor(num / 1000) + 'K';
|
||||
} else {
|
||||
// For numbers less than 1000
|
||||
return num.toString();
|
||||
}
|
||||
}
|
||||
|
||||
const TableOfModelsForProvider = ({ providerName }: { providerName: ProviderName }) => {
|
||||
|
||||
const accessor = useAccessor()
|
||||
const voidSettingsService = accessor.get('IVoidSettingsService')
|
||||
const voidSettingsState = useSettingsState()
|
||||
const isDetectableLocally = (refreshableProviderNames as ProviderName[]).includes(providerName)
|
||||
// const providerCapabilities = getProviderCapabilities(providerName)
|
||||
|
||||
|
||||
// info used to show the table
|
||||
const infoOfModelName: Record<string, { showAsDefault: boolean, isDownloaded: boolean } | undefined> = {}
|
||||
|
||||
voidSettingsState.settingsOfProvider[providerName].models.forEach(m => {
|
||||
infoOfModelName[m.modelName] = {
|
||||
showAsDefault: m.type !== 'custom',
|
||||
isDownloaded: true
|
||||
}
|
||||
})
|
||||
|
||||
// special case columns for ollama; show recommended models as default
|
||||
if (providerName === 'ollama') {
|
||||
for (const modelName of ollamaRecommendedModels) {
|
||||
if (modelName in infoOfModelName) continue
|
||||
infoOfModelName[modelName] = {
|
||||
isDownloaded: infoOfModelName[modelName]?.isDownloaded ?? false,
|
||||
showAsDefault: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return <table className="table-fixed border-collapse mb-6 bg-void-bg-2 text-sm mx-auto select-text">
|
||||
<thead>
|
||||
<tr className="border-b border-void-border-1 text-nowrap text-ellipsis">
|
||||
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[200px]">Models Offered</th>
|
||||
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Cost/M</th>
|
||||
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Context</th>
|
||||
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Chat</th>
|
||||
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Agent</th>
|
||||
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Autotab</th>
|
||||
{/* <th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Reasoning</th> */}
|
||||
{isDetectableLocally && <th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Detected</th>}
|
||||
{providerName === 'ollama' && <th className="text-left py-2 px-3 font-normal text-void-fg-3">Download</th>}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{Object.keys(infoOfModelName).map(modelName => {
|
||||
const { showAsDefault, isDownloaded } = infoOfModelName[modelName] ?? {}
|
||||
|
||||
|
||||
const capabilities = getModelCapabilities(providerName, modelName)
|
||||
const {
|
||||
downloadable,
|
||||
cost,
|
||||
supportsFIM,
|
||||
reasoningCapabilities,
|
||||
contextWindow,
|
||||
|
||||
isUnrecognizedModel,
|
||||
maxOutputTokens,
|
||||
supportsSystemMessage,
|
||||
} = capabilities
|
||||
|
||||
// TODO update this when tools work
|
||||
|
||||
const removeModelButton = <button
|
||||
className="absolute -left-1 top-1/2 transform -translate-y-1/2 -translate-x-full text-void-fg-3 hover:text-void-fg-1 text-xs"
|
||||
onClick={() => voidSettingsService.deleteModel(providerName, modelName)}
|
||||
>
|
||||
<X className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<tr key={`${modelName}${providerName}`} className="border-b border-void-border-1 hover:bg-void-bg-3/50">
|
||||
<td className="py-2 px-3 relative">
|
||||
{!showAsDefault && removeModelButton}
|
||||
{modelName}
|
||||
</td>
|
||||
<td className="py-2 px-3">${cost.output ?? ''}</td>
|
||||
<td className="py-2 px-3">{contextWindow ? abbreviateNumber(contextWindow) : ''}</td>
|
||||
<td className="py-2 px-3"><YesNoText val={true} /></td>
|
||||
<td className="py-2 px-3"><YesNoText val={!!true} /></td>
|
||||
<td className="py-2 px-3"><YesNoText val={!!supportsFIM} /></td>
|
||||
{/* <td className="py-2 px-3"><YesNoText val={!!reasoningCapabilities} /></td> */}
|
||||
{isDetectableLocally && <td className="py-2 px-3 flex items-center justify-center">{!!isDownloaded ? <Check className="w-4 h-4" /> : <></>}</td>}
|
||||
{providerName === 'ollama' && <th className="py-2 px-3">
|
||||
<OllamaDownloadOrRemoveModelButton modelName={modelName} isModelInstalled={!!infoOfModelName[modelName]?.isDownloaded} sizeGb={downloadable && downloadable.sizeGb} />
|
||||
</th>}
|
||||
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
<tr className="hover:bg-void-bg-3/50">
|
||||
<td className="py-2 px-3 text-void-accent">
|
||||
<ErrorBoundary>
|
||||
<AddModelInputBox
|
||||
key={providerName}
|
||||
providerName={providerName}
|
||||
compact={true}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</td>
|
||||
<td colSpan={4}></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
|
||||
|
||||
|
||||
const PrimaryActionButton = ({ children, className, ringSize, ...props }: { children: React.ReactNode, ringSize?: undefined | 'xl' | 'screen' } & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
|
||||
|
||||
|
||||
return (
|
||||
<button
|
||||
type='button'
|
||||
className={`
|
||||
flex items-center justify-center
|
||||
|
||||
text-white dark:text-black
|
||||
bg-black/90 dark:bg-white/90
|
||||
|
||||
${ringSize === 'xl' ? `
|
||||
gap-2 px-16 py-8
|
||||
hover:ring-8 active:ring-8
|
||||
transition-all duration-300 ease-in-out
|
||||
`
|
||||
: ringSize === 'screen' ? `
|
||||
gap-2 px-16 py-8
|
||||
ring-[3000px]
|
||||
transition-all duration-1000 ease-in-out
|
||||
`: ringSize === undefined ? `
|
||||
gap-1 px-4 py-2
|
||||
hover:ring-2 active:ring-2
|
||||
transition-all duration-300 ease-in-out
|
||||
`: ''}
|
||||
|
||||
hover:ring-black/90 dark:hover:ring-white/90
|
||||
active:ring-black/90 dark:active:ring-white/90
|
||||
|
||||
rounded-lg
|
||||
group
|
||||
${className}
|
||||
`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRight
|
||||
className={`
|
||||
transition-all duration-300 ease-in-out
|
||||
|
||||
transform
|
||||
group-hover:translate-x-1
|
||||
group-active:translate-x-1
|
||||
`}
|
||||
/>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
type WantToUseOption = 'smart' | 'private' | 'cheap' | 'all'
|
||||
|
||||
const VoidOnboardingContent = () => {
|
||||
|
||||
|
||||
const accessor = useAccessor()
|
||||
const voidSettingsService = accessor.get('IVoidSettingsService')
|
||||
|
||||
const voidSettingsState = useSettingsState()
|
||||
|
||||
const [pageIndex, setPageIndex] = useState(0)
|
||||
|
||||
|
||||
// page 1 state
|
||||
const [wantToUseOption, setWantToUseOption] = useState<WantToUseOption>('smart')
|
||||
|
||||
// Replace the single selectedProviderName with four separate states
|
||||
// page 2 state - each tab gets its own state
|
||||
const [selectedIntelligentProvider, setSelectedIntelligentProvider] = useState<ProviderName>('anthropic');
|
||||
const [selectedPrivateProvider, setSelectedPrivateProvider] = useState<ProviderName>('ollama');
|
||||
const [selectedAffordableProvider, setSelectedAffordableProvider] = useState<ProviderName>('gemini');
|
||||
const [selectedAllProvider, setSelectedAllProvider] = useState<ProviderName>('anthropic');
|
||||
|
||||
// Helper function to get the current selected provider based on active tab
|
||||
const getSelectedProvider = (): ProviderName => {
|
||||
switch (wantToUseOption) {
|
||||
case 'smart': return selectedIntelligentProvider;
|
||||
case 'private': return selectedPrivateProvider;
|
||||
case 'cheap': return selectedAffordableProvider;
|
||||
case 'all': return selectedAllProvider;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to set the selected provider for the current tab
|
||||
const setSelectedProvider = (provider: ProviderName) => {
|
||||
switch (wantToUseOption) {
|
||||
case 'smart': setSelectedIntelligentProvider(provider); break;
|
||||
case 'private': setSelectedPrivateProvider(provider); break;
|
||||
case 'cheap': setSelectedAffordableProvider(provider); break;
|
||||
case 'all': setSelectedAllProvider(provider); break;
|
||||
}
|
||||
}
|
||||
|
||||
const providerNamesOfWantToUseOption: { [wantToUseOption in WantToUseOption]: ProviderName[] } = {
|
||||
smart: ['anthropic', 'openAI', 'gemini', 'openRouter'],
|
||||
private: ['ollama', 'vLLM', 'openAICompatible', 'lmStudio'],
|
||||
cheap: ['gemini', 'deepseek', 'openRouter', 'ollama', 'vLLM'],
|
||||
all: providerNames,
|
||||
}
|
||||
|
||||
|
||||
const selectedProviderName = getSelectedProvider();
|
||||
const didFillInProviderSettings = selectedProviderName && voidSettingsState.settingsOfProvider[selectedProviderName]._didFillInProviderSettings
|
||||
const isApiKeyLongEnoughIfApiKeyExists = selectedProviderName && voidSettingsState.settingsOfProvider[selectedProviderName].apiKey ? voidSettingsState.settingsOfProvider[selectedProviderName].apiKey.length > 15 : true
|
||||
const isAtLeastOneModel = selectedProviderName && voidSettingsState.settingsOfProvider[selectedProviderName].models.length >= 1
|
||||
|
||||
const didFillInSelectedProviderSettings = !!(didFillInProviderSettings && isApiKeyLongEnoughIfApiKeyExists && isAtLeastOneModel)
|
||||
|
||||
const prevAndNextButtons = <div className="max-w-[600px] w-full mx-auto flex flex-col items-end">
|
||||
<div className="flex items-center gap-2">
|
||||
<PreviousButton
|
||||
onClick={() => { setPageIndex(pageIndex - 1) }}
|
||||
/>
|
||||
<NextButton
|
||||
onClick={() => { setPageIndex(pageIndex + 1) }}
|
||||
disabled={pageIndex === 2 && !didFillInSelectedProviderSettings}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
const lastPagePrevAndNextButtons = <div className="max-w-[600px] w-full mx-auto flex flex-col items-end">
|
||||
<div className="flex items-center gap-2">
|
||||
<PreviousButton
|
||||
onClick={() => { setPageIndex(pageIndex - 1) }}
|
||||
/>
|
||||
<PrimaryActionButton
|
||||
onClick={() => { voidSettingsService.setGlobalSetting('isOnboardingComplete', true); }}
|
||||
ringSize={voidSettingsState.globalSettings.isOnboardingComplete ? 'screen' : undefined}
|
||||
>Enter the Void</PrimaryActionButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
// cannot be md
|
||||
const basicDescOfWantToUseOption: { [wantToUseOption in WantToUseOption]: string } = {
|
||||
smart: "Models with the best performance on benchmarks.",
|
||||
private: "Host on your computer or local network for full data privacy.",
|
||||
cheap: "Free and affordable options.",
|
||||
all: "",
|
||||
}
|
||||
|
||||
// can be md
|
||||
const detailedDescOfWantToUseOption: { [wantToUseOption in WantToUseOption]: string } = {
|
||||
smart: "Most intelligent and best for agent mode.",
|
||||
private: "Private-hosted so your data never leaves your computer or network. [Email us](mailto:founders@voideditor.com) for help setting up at your company.",
|
||||
cheap: "Use great deals like Gemini 2.5 Pro, or self-host a model with Ollama or vLLM for free.",
|
||||
all: "",
|
||||
}
|
||||
|
||||
// Modified: initialize separate provider states on initial render instead of watching wantToUseOption changes
|
||||
useEffect(() => {
|
||||
if (selectedIntelligentProvider === undefined) {
|
||||
setSelectedIntelligentProvider(providerNamesOfWantToUseOption['smart'][0]);
|
||||
}
|
||||
if (selectedPrivateProvider === undefined) {
|
||||
setSelectedPrivateProvider(providerNamesOfWantToUseOption['private'][0]);
|
||||
}
|
||||
if (selectedAffordableProvider === undefined) {
|
||||
setSelectedAffordableProvider(providerNamesOfWantToUseOption['cheap'][0]);
|
||||
}
|
||||
if (selectedAllProvider === undefined) {
|
||||
setSelectedAllProvider(providerNamesOfWantToUseOption['all'][0]);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// reset the page to page 0 if the user redos onboarding
|
||||
useEffect(() => {
|
||||
if (!voidSettingsState.globalSettings.isOnboardingComplete) {
|
||||
setPageIndex(0)
|
||||
}
|
||||
}, [setPageIndex, voidSettingsState.globalSettings.isOnboardingComplete])
|
||||
|
||||
|
||||
const contentOfIdx: { [pageIndex: number]: React.ReactNode } = {
|
||||
0: <OnboardingPageShell
|
||||
content={
|
||||
<div className='flex flex-col items-center gap-8'>
|
||||
<div className="text-5xl font-light text-center">Welcome to Void</div>
|
||||
|
||||
{/* Slice of Void image */}
|
||||
<div className='max-w-md w-full h-[30vh] mx-auto flex items-center justify-center'>
|
||||
<VoidIcon />
|
||||
</div>
|
||||
|
||||
|
||||
<FadeIn
|
||||
delayMs={1000}
|
||||
>
|
||||
<PrimaryActionButton
|
||||
onClick={() => { setPageIndex(pageIndex + 1) }}
|
||||
>
|
||||
Get Started
|
||||
</PrimaryActionButton>
|
||||
</FadeIn>
|
||||
|
||||
</div>
|
||||
}
|
||||
/>,
|
||||
1: <OnboardingPageShell
|
||||
|
||||
hasMaxWidth={false}
|
||||
top={<></>}
|
||||
content={<div className='flex flex-col items-center -translate-y-[20vh]'>
|
||||
{/* <div className="text-5xl text-center mb-8">AI Preferences</div> */}
|
||||
|
||||
<div className="text-4xl text-void-fg-2 mb-8 text-center">Model Preferences</div>
|
||||
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 w-full max-w-[800px] mx-auto mt-8">
|
||||
<button
|
||||
onClick={() => { setWantToUseOption('smart'); setPageIndex(pageIndex + 1); }}
|
||||
className="flex flex-col p-6 rounded bg-void-bg-2 border border-void-border-3 hover:brightness-110 transition-colors focus:outline-none focus:border-void-accent-border relative overflow-hidden min-h-[160px]"
|
||||
>
|
||||
<div className="flex items-center mb-3">
|
||||
<Brain size={24} className="text-void-fg-2 mr-2" />
|
||||
<div className="text-lg font-medium text-void-fg-1">Intelligent</div>
|
||||
</div>
|
||||
<div className="text-sm text-void-fg-2 text-left">{basicDescOfWantToUseOption['smart']}</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => { setWantToUseOption('private'); setPageIndex(pageIndex + 1); }}
|
||||
className="flex flex-col p-6 rounded bg-void-bg-2 border border-void-border-3 hover:brightness-110 transition-colors focus:outline-none focus:border-void-accent-border relative overflow-hidden min-h-[160px]"
|
||||
>
|
||||
<div className="flex items-center mb-3">
|
||||
<Lock size={24} className="text-void-fg-2 mr-2" />
|
||||
<div className="text-lg font-medium text-void-fg-1">Private</div>
|
||||
</div>
|
||||
<div className="text-sm text-void-fg-2 text-left">{basicDescOfWantToUseOption['private']}</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => { setWantToUseOption('cheap'); setPageIndex(pageIndex + 1); }}
|
||||
className="flex flex-col p-6 rounded bg-void-bg-2 border border-void-border-3 hover:brightness-110 transition-colors focus:outline-none focus:border-void-accent-border relative overflow-hidden min-h-[160px]"
|
||||
>
|
||||
<div className="flex items-center mb-3">
|
||||
<DollarSign size={24} className="text-void-fg-2 mr-2" />
|
||||
<div className="text-lg font-medium text-void-fg-1">Affordable</div>
|
||||
</div>
|
||||
<div className="text-sm text-void-fg-2 text-left">{basicDescOfWantToUseOption['cheap']}</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
</div>}
|
||||
bottom={
|
||||
<div className='mx-auto w-full max-w-[800px]'>
|
||||
<PreviousButton onClick={() => { setPageIndex(pageIndex - 1) }} />
|
||||
</div>
|
||||
}
|
||||
/>,
|
||||
2: <OnboardingPageShell
|
||||
top={
|
||||
<>
|
||||
{/* Title */}
|
||||
|
||||
<div className="text-5xl font-light text-center mt-[10vh] mb-6">Choose a Provider</div>
|
||||
|
||||
{/* Preference Selector */}
|
||||
|
||||
<div
|
||||
className="mb-6 w-fit mx-auto flex items-center overflow-hidden bg-zinc-700/5 dark:bg-zinc-300/5 rounded-md"
|
||||
>
|
||||
{[
|
||||
{ id: 'smart', label: 'Intelligent' },
|
||||
{ id: 'private', label: 'Private' },
|
||||
{ id: 'cheap', label: 'Affordable' },
|
||||
{ id: 'all', label: 'All' }
|
||||
].map(option => (
|
||||
<ErrorBoundary
|
||||
key={option.id}
|
||||
>
|
||||
<button
|
||||
onClick={() => setWantToUseOption(option.id as WantToUseOption)}
|
||||
className={`py-1 px-2 text-xs cursor-pointer whitespace-nowrap rounded-sm transition-colors ${wantToUseOption === option.id
|
||||
? 'dark:text-white text-black font-medium'
|
||||
: 'text-void-fg-3 hover:text-void-fg-2'
|
||||
}`}
|
||||
data-tooltip-id='void-tooltip'
|
||||
data-tooltip-content={`${option.label} providers`}
|
||||
data-tooltip-place='bottom'
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
</ErrorBoundary>
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{/* Provider Buttons - Modified to use separate components for each tab */}
|
||||
<div className="mb-2 w-full">
|
||||
{/* Intelligent tab */}
|
||||
<ErrorBoundary>
|
||||
<div className={`flex flex-wrap items-center w-full ${wantToUseOption === 'smart' ? 'flex' : 'hidden'}`}>
|
||||
{providerNamesOfWantToUseOption['smart'].map((providerName) => {
|
||||
const isSelected = selectedIntelligentProvider === providerName;
|
||||
return (
|
||||
<button
|
||||
key={providerName}
|
||||
onClick={() => setSelectedIntelligentProvider(providerName)}
|
||||
className={`py-[2px] px-2 mx-0.5 my-0.5 text-xs font-medium cursor-pointer relative rounded-full transition-all duration-300
|
||||
${isSelected ? 'bg-zinc-100 text-zinc-900 shadow-sm border-white/80' : 'bg-zinc-100/40 hover:bg-zinc-100/50 text-zinc-900 border-white/20'}`}
|
||||
>
|
||||
{displayInfoOfProviderName(providerName).title}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
|
||||
|
||||
{/* Private tab */}
|
||||
<ErrorBoundary>
|
||||
<div className={`flex flex-wrap items-center w-full ${wantToUseOption === 'private' ? 'flex' : 'hidden'}`}>
|
||||
{providerNamesOfWantToUseOption['private'].map((providerName) => {
|
||||
const isSelected = selectedPrivateProvider === providerName;
|
||||
return (
|
||||
<button
|
||||
key={providerName}
|
||||
onClick={() => setSelectedPrivateProvider(providerName)}
|
||||
className={`py-[2px] px-2 mx-0.5 my-0.5 text-xs font-medium cursor-pointer relative rounded-full transition-all duration-300
|
||||
${isSelected ? 'bg-zinc-100 text-zinc-900 shadow-sm border-white/80' : 'bg-zinc-100/40 hover:bg-zinc-100/50 text-zinc-900 border-white/20'}`}
|
||||
>
|
||||
{displayInfoOfProviderName(providerName).title}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
|
||||
|
||||
{/* Affordable tab */}
|
||||
<ErrorBoundary>
|
||||
|
||||
<div className={`flex flex-wrap items-center w-full ${wantToUseOption === 'cheap' ? 'flex' : 'hidden'}`}>
|
||||
{providerNamesOfWantToUseOption['cheap'].map((providerName) => {
|
||||
const isSelected = selectedAffordableProvider === providerName;
|
||||
return (
|
||||
<button
|
||||
key={providerName}
|
||||
onClick={() => setSelectedAffordableProvider(providerName)}
|
||||
className={`py-[2px] px-2 mx-0.5 my-0.5 text-xs font-medium cursor-pointer relative rounded-full transition-all duration-300
|
||||
${isSelected ? 'bg-zinc-100 text-zinc-900 shadow-sm border-white/80' : 'bg-zinc-100/40 hover:bg-zinc-100/50 text-zinc-900 border-white/20'}`}
|
||||
>
|
||||
{displayInfoOfProviderName(providerName).title}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
|
||||
|
||||
{/* All tab */}
|
||||
<ErrorBoundary>
|
||||
<div className={`flex flex-wrap items-center w-full ${wantToUseOption === 'all' ? 'flex' : 'hidden'}`}>
|
||||
{providerNames.map((providerName) => {
|
||||
const isSelected = selectedAllProvider === providerName;
|
||||
return (
|
||||
<button
|
||||
key={providerName}
|
||||
onClick={() => setSelectedAllProvider(providerName)}
|
||||
className={`py-[2px] px-2 mx-0.5 my-0.5 text-xs font-medium cursor-pointer relative rounded-full transition-all duration-300
|
||||
${isSelected ? 'bg-zinc-100 text-zinc-900 shadow-sm border-white/80' : 'bg-zinc-100/40 hover:bg-zinc-100/50 text-zinc-900 border-white/20'}`}
|
||||
>
|
||||
{displayInfoOfProviderName(providerName).title}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<ErrorBoundary>
|
||||
<div className="text-left self-start text-sm text-void-fg-3 px-2 py-1">
|
||||
<ChatMarkdownRender string={detailedDescOfWantToUseOption[wantToUseOption]} chatMessageLocation={undefined} />
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
|
||||
|
||||
{/* ModelsTable and ProviderFields */}
|
||||
{selectedProviderName && <div className='mt-4 w-fit mx-auto'>
|
||||
{/* Models Table */}
|
||||
<ErrorBoundary>
|
||||
<TableOfModelsForProvider providerName={selectedProviderName} />
|
||||
</ErrorBoundary>
|
||||
|
||||
|
||||
{/* Add provider section - simplified styling */}
|
||||
|
||||
<div className='mb-5 mt-8 mx-auto'>
|
||||
<ErrorBoundary>
|
||||
<div className=''>
|
||||
Add {displayInfoOfProviderName(selectedProviderName).title}
|
||||
|
||||
<div className='my-4'>
|
||||
{selectedProviderName === 'ollama' ? <OllamaSetupInstructions /> : ''}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
|
||||
<ErrorBoundary>
|
||||
{selectedProviderName &&
|
||||
<SettingsForProvider providerName={selectedProviderName} showProviderTitle={false} showProviderSuggestions={false} />
|
||||
}
|
||||
</ErrorBoundary>
|
||||
|
||||
{/* Button and status indicators */}
|
||||
<ErrorBoundary>
|
||||
{!didFillInProviderSettings ? <p className="text-xs text-void-fg-3 mt-2">Please fill in all fields to continue</p>
|
||||
: !isAtLeastOneModel ? <p className="text-xs text-void-fg-3 mt-2">Please add a model to continue</p>
|
||||
: !isApiKeyLongEnoughIfApiKeyExists ? <p className="text-xs text-void-fg-3 mt-2">Please enter a valid API key</p>
|
||||
: <AnimatedCheckmarkButton className='text-xs text-void-fg-3 mt-2' text='Added' />}
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
|
||||
</div>}
|
||||
</>
|
||||
}
|
||||
|
||||
bottom={
|
||||
<ErrorBoundary>
|
||||
<FadeIn delayMs={50} durationMs={10}>
|
||||
{prevAndNextButtons}
|
||||
</FadeIn>
|
||||
</ErrorBoundary>
|
||||
}
|
||||
|
||||
/>,
|
||||
|
||||
// 2.5: <div className="max-w-[600px] w-full h-full text-left mx-auto flex flex-col items-center justify-between">
|
||||
// <FadeIn>
|
||||
// <div className="text-5xl font-light mb-6 mt-12 text-center">Autocomplete</div>
|
||||
|
||||
// <div className="text-center flex flex-col gap-4 w-full max-w-md mx-auto">
|
||||
// <h4 className="text-void-fg-3 mb-2">Void offers free autocomplete with locally hosted models</h4>
|
||||
// <h4 className="text-void-fg-3 mb-2">[have buttons for Ollama install Qwen2.5coder3b and memory requirements] </h4>
|
||||
|
||||
// </div>
|
||||
// </FadeIn>
|
||||
|
||||
// {prevAndNextButtons}
|
||||
// </div>,
|
||||
3: <OnboardingPageShell
|
||||
|
||||
content={
|
||||
<div>
|
||||
<div className="text-5xl font-light text-center">Settings and Themes</div>
|
||||
|
||||
<div className="mt-8 text-center flex flex-col items-center gap-4 w-full max-w-md mx-auto">
|
||||
<h4 className="text-void-fg-3 mb-4">Transfer your settings from an existing editor?</h4>
|
||||
<OneClickSwitchButton className='w-full px-4 py-2' fromEditor="VS Code" />
|
||||
<OneClickSwitchButton className='w-full px-4 py-2' fromEditor="Cursor" />
|
||||
<OneClickSwitchButton className='w-full px-4 py-2' fromEditor="Windsurf" />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
bottom={lastPagePrevAndNextButtons}
|
||||
// bottom={prevAndNextButtons}
|
||||
/>,
|
||||
// 4: <OnboardingPageShell
|
||||
// content={
|
||||
// <>
|
||||
// <div
|
||||
// className='flex justify-center'
|
||||
// >
|
||||
// <PrimaryActionButton
|
||||
// onClick={() => { voidSettingsService.setGlobalSetting('isOnboardingComplete', true); }}
|
||||
// ringSize={voidSettingsState.globalSettings.isOnboardingComplete ? 'screen' : undefined}
|
||||
// className='text-4xl'
|
||||
// >Enter the Void</PrimaryActionButton>
|
||||
// </div>
|
||||
// </>
|
||||
// }
|
||||
// bottom={
|
||||
// <PreviousButton
|
||||
// onClick={() => { setPageIndex(pageIndex - 1) }}
|
||||
// />
|
||||
// }
|
||||
// />,
|
||||
}
|
||||
|
||||
|
||||
return <div key={pageIndex} className="w-full h-full text-left mx-auto overflow-y-scroll flex flex-col items-center justify-around">
|
||||
<ErrorBoundary>
|
||||
{contentOfIdx[pageIndex]}
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { mountFnGenerator } from '../util/mountFnGenerator.js'
|
||||
import { VoidOnboarding } from './VoidOnboarding.js'
|
||||
|
||||
export const mountVoidOnboarding = mountFnGenerator(VoidOnboarding)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user