Compare commits
217 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5257e9c82d | |||
| 3aa6d3d28a | |||
| 98c1a7e528 | |||
| c94dda65f7 | |||
| 24e276ba47 | |||
| 028061c879 | |||
| 4e0dd8f8db | |||
| c239682ad8 | |||
| 3bfb50e6ee | |||
| 362d481ec4 | |||
| 1b4943f39e | |||
| 8c443ab780 | |||
| 7519f05238 | |||
| c2fc2577e0 | |||
| 6e1f8a08b3 | |||
| d9be67d17d | |||
| 60d22674e2 | |||
| e8c0ee631c | |||
| 6ba9d10a4a | |||
| 4eff8f8e76 | |||
| 66e349c72e | |||
| 146d63c41e | |||
| 82439406a0 | |||
| efeb0d28b7 | |||
| aba7d7bf90 | |||
| f517dc79a9 | |||
| dc33d4d0c0 | |||
| d3647bf93c | |||
| 29c4603909 | |||
| 85426e28bd | |||
| c6ff563e1b | |||
| 7ec574eeee | |||
| 66c880945c | |||
| 6f1905a962 | |||
| 268d2ae99a | |||
| 1521038e03 | |||
| dce0378eb2 | |||
| ce7ee62abe | |||
| 015943329f | |||
| 4b15885147 | |||
| 2e3f608805 | |||
| 789016709a | |||
| 835a2bdf92 | |||
| c02f1de758 | |||
| ddd9cf857f | |||
| e4fd6d05a4 | |||
| 9756ab6d16 | |||
| 433884b36c | |||
| 0d48db1a0c | |||
| 10adc13b60 | |||
| 2103a95930 | |||
| f8c454544c | |||
| e3da020c0a | |||
| 7dec57ae04 | |||
| f8db386c3b | |||
| 2d583b87dc | |||
| b9dba7d08b | |||
| 4e18dd4ac3 | |||
| 471333ec16 | |||
| d43453e5ed | |||
| 3abafa76ec | |||
| e48e798afc | |||
| 4619e4146a | |||
| f522281c6d | |||
| 1e3668058b | |||
| 88c921b059 | |||
| 8cfe494632 | |||
| d4fc59034a | |||
| 9ba70cdc4a | |||
| 12d08eb709 | |||
| f3270a2219 | |||
| bd30f14980 | |||
| 725de600cf | |||
| ccfd54fcfe | |||
| 071c4893c2 | |||
| 8f15c67960 | |||
| fd546ff7b7 | |||
| 895628bb91 | |||
| 4898875532 | |||
| 9ddc22b63c | |||
| 44438fe1fe | |||
| 64e43fa53e | |||
| 07f4a59ce6 | |||
| c7d5d5d183 | |||
| 9f96ebcfc2 | |||
| 01839654d4 | |||
| 37200dacbd | |||
| 86612fd5ed | |||
| 937816519c | |||
| 35e366a5ba | |||
| d2fdbd7ffd | |||
| 33a003fc2d | |||
| 9589134830 | |||
| 164684a39b | |||
| accff821f8 | |||
| 00fc9964e5 | |||
| 3e5ead2547 | |||
| 52d89e5815 | |||
| 4cf13b67b0 | |||
| fc327e3fcb | |||
| 59afe2d6ff | |||
| 1d590510ae | |||
| c5a6f476ef | |||
| ae4ece1826 | |||
| ec2f04be91 | |||
| 8a56ea9755 | |||
| 261736bde5 | |||
| 7dd5171bd2 | |||
| becd109776 | |||
| 0d9f441967 | |||
| 6386fc95ee | |||
| 06688228ae | |||
| b3d2453e7f | |||
| 29e1f312c3 | |||
| b9b645522a | |||
| 83bf660b76 | |||
| 268f08c51d | |||
| 046942be75 | |||
| 6fed97eb83 | |||
| e888f261fe | |||
| 6e8498414c | |||
| bcda1dbe2b | |||
| be3128acb4 | |||
| 29cbb15911 | |||
| 8da71dc1b7 | |||
| a69b99b562 | |||
| 6868a8b1d8 | |||
| 10f8fcc17d | |||
| ee9f84a475 | |||
| c056214684 | |||
| fcf03f2710 | |||
| ed50ed5319 | |||
| f15f5ce8ed | |||
| 11578d6288 | |||
| d904dbb93e | |||
| 6df0c0ac69 | |||
| 0f98af7fc1 | |||
| e1b8e49b13 | |||
| e6f6c7500d | |||
| 951da8e31a | |||
| 966470cf68 | |||
| b2cf709685 | |||
| ab0bf4bef0 | |||
| db20584a19 | |||
| 7d1d638dae | |||
| 0d1c4944c1 | |||
| f7c4d28d55 | |||
| a2ae7a9674 | |||
| 262e15764b | |||
| 26f97f5939 | |||
| a9dc35e5db | |||
| 326dcae24f | |||
| f880ddd6dc | |||
| 7925e438c6 | |||
| 2f5e954ebf | |||
| 6a8ac1a68e | |||
| c5b6fd9b75 | |||
| 7e68a485f2 | |||
| a1da8bd12f | |||
| ebd78a902d | |||
| f96c03c846 | |||
| fed865e753 | |||
| 8d2917d331 | |||
| f4c1fc8917 | |||
| 0a3f31acae | |||
| 924c953697 | |||
| 446dede5e4 | |||
| 7bb0993249 | |||
| 4968452003 | |||
| 037f9f38e5 | |||
| e132c87792 | |||
| 07d87c8bf0 | |||
| c7ad7d3761 | |||
| ff9d21eee0 | |||
| e4953057ef | |||
| 865c34f282 | |||
| ec65903692 | |||
| a08e6078f2 | |||
| 20e1b287f8 | |||
| f2f9b6ccca | |||
| c4e4701dc3 | |||
| 37af31f958 | |||
| b5dff4cf0f | |||
| 3621bb63f4 | |||
| da247c6ed4 | |||
| ce634eadaa | |||
| 910ed7faa9 | |||
| 9e852355d9 | |||
| 8ae4700bb2 | |||
| f13c30ae0d | |||
| af244938b8 | |||
| a53f92432b | |||
| ca2b18d9da | |||
| 5293095eb8 | |||
| 86346f1bf0 | |||
| a0672f845d | |||
| f96d320ce0 | |||
| bb7e69b20b | |||
| f19e579ce9 | |||
| dd133d2cd5 | |||
| 2ba04a78a8 | |||
| fb948b331f | |||
| 5083b8e971 | |||
| 35091eb8f2 | |||
| b9e61bf4e2 | |||
| 55ec3365a2 | |||
| d6811e872a | |||
| c176b8020f | |||
| 85c2c80db0 | |||
| b677d3dfc1 | |||
| 6dcf684429 | |||
| 26ddd6418c | |||
| cffe558cf5 | |||
| 9bc551f0ac | |||
| 01d7878671 | |||
| 2519f094cc | |||
| a72305819a |
+34
-30
@@ -2,17 +2,18 @@
|
||||
|
||||
Welcome! 👋 This is the official guide on how to contribute to Void. We want to make it as easy as possible to contribute, so if you have any questions or comments, reach out via email or discord!
|
||||
|
||||
|
||||
|
||||
There are a few ways to contribute:
|
||||
|
||||
- 👨💻 Build new features - see [Issues](https://github.com/voideditor/void/issues).
|
||||
- 💡 Make suggestions in our [Discord](https://discord.gg/RSNjgaugJs).
|
||||
- ⭐️ If you want to build your AI tool into Void, feel free to get in touch! It's very easy to extend Void, and the UX you create will be much more natural than a VSCode Extension.
|
||||
|
||||
Void's code lives in `src/vs/workbench/contrib/void/browser/` and `src/vs/platform/void/`.
|
||||
Void's code mostly lives in `src/vs/workbench/contrib/void/` and `src/vs/platform/void/`.
|
||||
|
||||
|
||||
|
||||
## Building the full IDE
|
||||
## Building Void
|
||||
|
||||
### a. Build Prerequisites - Mac
|
||||
|
||||
@@ -41,21 +42,22 @@ First, run `npm install -g node-gyp`. Then:
|
||||
- Red Hat (Fedora, etc): `sudo dnf install @development-tools gcc gcc-c++ make libsecret-devel krb5-devel libX11-devel libxkbfile-devel`.
|
||||
- Others: see [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute).
|
||||
|
||||
### Building Void
|
||||
### d. Building Void
|
||||
|
||||
To build Void, open `void/` inside VSCode. Then open your terminal and run:
|
||||
|
||||
1. `npm install` to install all dependencies.
|
||||
2. `npm run watchreact` to build Void's browser dependencies like React.
|
||||
2. `npm run watchreact` to build Void's browser dependencies like React. (If this doesn't work, try `npm run buildreact`).
|
||||
3. Build Void.
|
||||
- Press <kbd>Cmd+Shift+B</kbd> (Mac).
|
||||
- Press <kbd>Ctrl+Shift+B</kbd> (Windows/Linux).
|
||||
- This step can take ~5 min. The build is done when you see two check marks.
|
||||
4. Run Void.
|
||||
- Run `./scripts/code.sh` (Mac/Linux).
|
||||
- Run `./scripts/code.sh` (Mac/Linux).
|
||||
- Run `./scripts/code.bat` (Windows).
|
||||
- This command should open up the built IDE. You can always press <kbd>Ctrl+R</kbd> (<kbd>Cmd+R</kbd>) inside the new window to see changes without re-building, or press or <kbd>Ctrl+Shift+P</kbd> in the new window and run "Reload Window".
|
||||
- If you are actively developing Void, we strongly recommend adding the flags `--user-data-dir ./.tmp/user-data --extensions-dir ./.tmp/extensions` to the above run command (just append them at the end of the string). This will save all data and extensions to the `.tmp` folder. You can delete this folder to reset any IDE changes you made when testing.
|
||||
6. Nice-to-knows.
|
||||
- You can always press <kbd>Ctrl+R</kbd> (<kbd>Cmd+R</kbd>) inside the new window to reload and see your new changes. It's faster than <kbd>Ctrl+Shift+P</kbd> and `Reload Window`.
|
||||
- You might want to add the flags `--user-data-dir ./.tmp/user-data --extensions-dir ./.tmp/extensions` to the above run command, which lets you delete the `.tmp` folder to reset any IDE changes you made when testing.
|
||||
|
||||
#### Building Void from Terminal
|
||||
|
||||
@@ -70,28 +72,19 @@ Alternatively, if you want to build Void from the terminal, instead of pressing
|
||||
|
||||
|
||||
|
||||
### Common Fixes
|
||||
#### Common Fixes
|
||||
|
||||
- Make sure you follow the prerequisite steps.
|
||||
- Make sure you followed the prerequisite steps.
|
||||
- Make sure you have the same NodeJS version as `.nvmrc`.
|
||||
- Make sure your `npm run watchreact` is running if you change any React files, or else you'll need to re-build.
|
||||
- If you see missing styles, go to `src2/styles.css` and re-save the file.
|
||||
- If you get `"TypeError: Failed to fetch dynamically imported module: vscode-file://vscode-app/.../workbench.desktop.main.js", source: file:///.../bootstrap-window.js`, make sure all imports end with `.js`.
|
||||
- If you have any questions, feel free to [submit an issue](https://github.com/voideditor/void/issues/new). For building questions, you can also refer to VSCode's full [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute) page.
|
||||
- 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.
|
||||
|
||||
|
||||
|
||||
## Bundling
|
||||
## Packaging
|
||||
|
||||
We don't usually recommend bundling. Instead, you should probably just build. If you're sure you want to bundle Void into an executable app, make sure you've built first, then run one of the following commands. This will create a folder named `VSCode-darwin-arm64` or similar outside of the void/ repo (see below). Be patient - compiling can take ~25 minutes.
|
||||
|
||||
```bash
|
||||
workspace/
|
||||
├── void/ # Your Void Fork
|
||||
├── VSCode-linux-x64/ # Build folder generated outside of void for Linux
|
||||
└── VSCode-darwin-arm64/ # Build folder generated outside of void for Mac
|
||||
|
||||
```
|
||||
We don't usually recommend packaging. Instead, you should probably just build. If you're sure you want to package Void into an executable app, make sure you've built first, then run one of the following commands. This will create a folder named `VSCode-darwin-arm64` or similar outside of the void/ repo (see below). Be patient - packaging can take ~25 minutes.
|
||||
|
||||
|
||||
### Mac
|
||||
@@ -108,16 +101,24 @@ workspace/
|
||||
- `npm run gulp vscode-linux-ia32`
|
||||
|
||||
|
||||
### Output
|
||||
|
||||
# Guidelines
|
||||
This will generate a folder outside of `void/`:
|
||||
```bash
|
||||
workspace/
|
||||
├── void/ # Your Void fork
|
||||
└── VSCode-darwin-arm64/ # Generated output
|
||||
```
|
||||
|
||||
We're always glad to talk about new ideas, help you get set up, and make sure your changes align with our vision for the project! Feel free to shoot Mat or Andrew a message, or start chatting with us in the `#contributing` channel of our [Discord](https://discord.gg/RSNjgaugJs).
|
||||
### Distributing
|
||||
We have a few scripts to convert `Mac .app -> .dmg`, `Windows folder -> .exe`, and `Linux folder -> appimage` for distribution. Let us know if you'd like access to them.
|
||||
|
||||
## Pull Request Guidelines
|
||||
|
||||
|
||||
## Submitting a Pull Request
|
||||
|
||||
- Please submit a pull request once you've made a change. No need to submit an Issue unless you're creating a new feature.
|
||||
- Please don't use AI to write your PR 🙂.
|
||||
- Please submit a pull request once you've made a change.
|
||||
- No need to submit an Issue unless you're creating a new feature.
|
||||
- Please don't use AI to write your PR 🙂
|
||||
|
||||
|
||||
<!--
|
||||
@@ -125,6 +126,8 @@ We're always glad to talk about new ideas, help you get set up, and make sure yo
|
||||
|
||||
We keep track of all the files we've changed with Void so it's easy to rebase:
|
||||
|
||||
Edit: far too many changes to track... this is old
|
||||
|
||||
- README.md
|
||||
- CONTRIBUTING.md
|
||||
- VOID_USEFUL_LINKS.md
|
||||
@@ -144,6 +147,7 @@ We keep track of all the files we've changed with Void so it's easy to rebase:
|
||||
- build/npm/dirs.js
|
||||
|
||||
- vscode.proposed.editorInsets.d.ts - not modified, but code copied
|
||||
|
||||
-->
|
||||
|
||||
## References
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
Void is a fork of VS Code, which is licensed under the MIT License (below).
|
||||
Void's additions and modifications are licensed under the MIT License (see LICENSE.txt).
|
||||
Void's additions and modifications are licensed under the Apache 2.0 License (see LICENSE.txt).
|
||||
|
||||
--------------------
|
||||
|
||||
|
||||
+197
-17
@@ -1,21 +1,201 @@
|
||||
MIT License
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
Copyright (c) 2025 Glass Devtools, Inc.
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
1. Definitions.
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2025 Glass Devtools, Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
@@ -1,17 +1,39 @@
|
||||
# Welcome to Void.
|
||||
|
||||
Void is the open-source Cursor alternative. This repo contains the full sourcecode for Void. We have a [waitlist](https://voideditor.com/email) for downloading the official release, but you can build and develop Void right now.
|
||||
<div align="center">
|
||||
<img
|
||||
src="./src/vs/workbench/browser/parts/editor/media/slice_of_void.png"
|
||||
alt="Void Welcome"
|
||||
width="300"
|
||||
height="300"
|
||||
/>
|
||||
</div>
|
||||
|
||||
If you're new, welcome!
|
||||
Void is the open-source Cursor alternative. This repo contains the full sourcecode for Void. We are currently in [open beta](https://voideditor.com/) for downloading the official release. If you're new, welcome!
|
||||
|
||||
- 👋 [Discord](https://discord.gg/RSNjgaugJs)
|
||||
|
||||
- 🚙 [Roadmap](https://github.com/orgs/voideditor/projects/2)
|
||||
|
||||
- 📝 [Changelog](https://voideditor.com/changelog)
|
||||
|
||||
- 🔨 [Contribute](https://github.com/voideditor/void/blob/main/CONTRIBUTING.md)
|
||||
|
||||
|
||||
<!-- ❤️ Setup: -->
|
||||
|
||||
## Contributing
|
||||
|
||||
To build and run Void, follow the steps in [`CONTRIBUTING.md`](https://github.com/voideditor/void/blob/main/CONTRIBUTING.md).
|
||||
|
||||
1. To contribute to the Void repository, follow the steps in [`CONTRIBUTING.md`](https://github.com/voideditor/void/blob/main/CONTRIBUTING.md).
|
||||
|
||||
2. To suggest a new feature, please create a new [GitHub Issue](https://github.com/voideditor/void/issues). Suggestions will be added to our Roadmap within one day.
|
||||
|
||||
3. We're open to collaborations of all types. Feel free to contact us via email or discord.
|
||||
|
||||
## Reference
|
||||
|
||||
Void is a fork of the of [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 some useful links on VSCode, see [`VOID_USEFUL_LINKS.md`](https://github.com/voideditor/void/blob/main/VOID_USEFUL_LINKS.md).
|
||||
|
||||
## Support
|
||||
Feel free to reach out in our [Discord](https://discord.gg/RSNjgaugJs) server or contact us via email.
|
||||
Feel free to reach out in our discord or contact us via email.
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# Void - this looks like the relevant file for us (product-build-darwin.yml is independent and maybe just used for testing)
|
||||
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
inputs:
|
||||
@@ -59,6 +61,8 @@ steps:
|
||||
- script: node build/azure-pipelines/distro/mixin-quality
|
||||
displayName: Mixin distro quality
|
||||
|
||||
|
||||
## Void - IMPORTANT
|
||||
- script: |
|
||||
set -e
|
||||
unzip $(Pipeline.Workspace)/unsigned_vscode_client_darwin_x64_archive/VSCode-darwin-x64.zip -d $(agent.builddirectory)/VSCode-darwin-x64
|
||||
@@ -66,6 +70,7 @@ steps:
|
||||
DEBUG=* node build/darwin/create-universal-app.js $(agent.builddirectory)
|
||||
displayName: Create Universal App
|
||||
|
||||
## Void - IMPORTANT
|
||||
- script: |
|
||||
set -e
|
||||
security create-keychain -p pwd $(agent.tempdirectory)/buildagent.keychain
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
// Void explanation - product-build-darwin-universal.yml runs this (create-universal-app.ts), then sign.ts
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const minimatch = require("minimatch");
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// Void explanation - product-build-darwin-universal.yml runs this (create-universal-app.ts), then sign.ts
|
||||
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import * as minimatch from 'minimatch';
|
||||
|
||||
@@ -78,24 +78,24 @@ async function main(buildDir) {
|
||||
// universal will get its copy from the x64 build.
|
||||
if (arch !== 'universal') {
|
||||
await (0, cross_spawn_promise_1.spawn)('plutil', [
|
||||
'-insert',
|
||||
'-replace', // Void changed this to replace
|
||||
'NSAppleEventsUsageDescription',
|
||||
'-string',
|
||||
'An application in Visual Studio Code wants to use AppleScript.',
|
||||
'An application in Void wants to use AppleScript.',
|
||||
`${infoPlistPath}`
|
||||
]);
|
||||
await (0, cross_spawn_promise_1.spawn)('plutil', [
|
||||
'-replace',
|
||||
'NSMicrophoneUsageDescription',
|
||||
'-string',
|
||||
'An application in Visual Studio Code wants to use the Microphone.',
|
||||
'An application in Void wants to use the Microphone.',
|
||||
`${infoPlistPath}`
|
||||
]);
|
||||
await (0, cross_spawn_promise_1.spawn)('plutil', [
|
||||
'-replace',
|
||||
'NSCameraUsageDescription',
|
||||
'-string',
|
||||
'An application in Visual Studio Code wants to use the Camera.',
|
||||
'An application in Void wants to use the Camera.',
|
||||
`${infoPlistPath}`
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -89,24 +89,24 @@ async function main(buildDir?: string): Promise<void> {
|
||||
// universal will get its copy from the x64 build.
|
||||
if (arch !== 'universal') {
|
||||
await spawn('plutil', [
|
||||
'-insert',
|
||||
'-replace', // Void changed this to replace
|
||||
'NSAppleEventsUsageDescription',
|
||||
'-string',
|
||||
'An application in Visual Studio Code wants to use AppleScript.',
|
||||
'An application in Void wants to use AppleScript.',
|
||||
`${infoPlistPath}`
|
||||
]);
|
||||
await spawn('plutil', [
|
||||
'-replace',
|
||||
'NSMicrophoneUsageDescription',
|
||||
'-string',
|
||||
'An application in Visual Studio Code wants to use the Microphone.',
|
||||
'An application in Void wants to use the Microphone.',
|
||||
`${infoPlistPath}`
|
||||
]);
|
||||
await spawn('plutil', [
|
||||
'-replace',
|
||||
'NSCameraUsageDescription',
|
||||
'-string',
|
||||
'An application in Visual Studio Code wants to use the Camera.',
|
||||
'An application in Void wants to use the Camera.',
|
||||
`${infoPlistPath}`
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -341,6 +341,8 @@ function packageTask(platform, arch, sourceFolderName, destinationFolderName, op
|
||||
this.emit('data', file);
|
||||
}));
|
||||
|
||||
|
||||
// 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 }))
|
||||
|
||||
@@ -23,7 +23,7 @@ AppMutex={code:GetAppMutex}
|
||||
SetupMutex={#AppMutex}setup
|
||||
; this is a Void icon comment. Old: WizardImageFile="{#RepoDir}\resources\win32\inno-big-100.bmp,{#RepoDir}\resources\win32\inno-big-125.bmp,{#RepoDir}\resources\win32\inno-big-150.bmp,{#RepoDir}\resources\win32\inno-big-175.bmp,{#RepoDir}\resources\win32\inno-big-200.bmp,{#RepoDir}\resources\win32\inno-big-225.bmp,{#RepoDir}\resources\win32\inno-big-250.bmp"
|
||||
; this is a Void icon comment. Old: WizardSmallImageFile="{#RepoDir}\resources\win32\inno-small-100.bmp,{#RepoDir}\resources\win32\inno-small-125.bmp,{#RepoDir}\resources\win32\inno-small-150.bmp,{#RepoDir}\resources\win32\inno-small-175.bmp,{#RepoDir}\resources\win32\inno-small-200.bmp,{#RepoDir}\resources\win32\inno-small-225.bmp,{#RepoDir}\resources\win32\inno-small-250.bmp"
|
||||
WizardImageFile="{#RepoDir}\resources\win32\inno-void.bmp"
|
||||
; WizardImageFile="{#RepoDir}\resources\win32\inno-void.bmp"
|
||||
WizardSmallImageFile="{#RepoDir}\resources\win32\inno-void.bmp"
|
||||
SetupIconFile={#RepoDir}\resources\win32\code.ico
|
||||
UninstallDisplayIcon={app}\{#ExeBasename}.exe
|
||||
|
||||
@@ -1,333 +0,0 @@
|
||||
import * as vscode from 'vscode';
|
||||
import Parser from 'tree-sitter';
|
||||
import JavaScript from 'tree-sitter-javascript';
|
||||
|
||||
interface Definition {
|
||||
file: string;
|
||||
node: Parser.SyntaxNode;
|
||||
}
|
||||
|
||||
interface DefnUse {
|
||||
parent: Parser.SyntaxNode;
|
||||
file: string;
|
||||
}
|
||||
|
||||
interface ImportInfo {
|
||||
source: string;
|
||||
imported: string;
|
||||
}
|
||||
|
||||
class ProjectAnalyzer {
|
||||
private parser: Parser;
|
||||
private graph: Map<string, Set<string>>;
|
||||
private visited: Set<string>;
|
||||
private parsedFiles: Map<string, Parser.Tree>;
|
||||
private imports: Map<string, Map<string, ImportInfo>>;
|
||||
private definitions: Map<string, Definition>;
|
||||
private fileStack: Set<string>;
|
||||
|
||||
constructor() {
|
||||
this.parser = new Parser();
|
||||
this.parser.setLanguage(JavaScript);
|
||||
this.graph = new Map();
|
||||
this.visited = new Set();
|
||||
this.parsedFiles = new Map();
|
||||
this.imports = new Map();
|
||||
this.definitions = new Map();
|
||||
this.fileStack = new Set();
|
||||
}
|
||||
|
||||
async parseFile(filePath: string): Promise<Parser.Tree | null> {
|
||||
if (this.parsedFiles.has(filePath)) {
|
||||
return this.parsedFiles.get(filePath)!;
|
||||
}
|
||||
|
||||
if (this.fileStack.has(filePath)) {
|
||||
return null; // Circular import
|
||||
}
|
||||
|
||||
this.fileStack.add(filePath);
|
||||
|
||||
try {
|
||||
const uri = vscode.Uri.file(filePath);
|
||||
const document = await vscode.workspace.openTextDocument(uri);
|
||||
const code = document.getText();
|
||||
const tree = this.parser.parse(code);
|
||||
|
||||
this.parsedFiles.set(filePath, tree);
|
||||
this.collectImports(filePath, tree);
|
||||
this.collectDefinitions(filePath, tree);
|
||||
|
||||
return tree;
|
||||
} catch (error) {
|
||||
console.error(`Error parsing ${filePath}:`, error);
|
||||
return null;
|
||||
} finally {
|
||||
this.fileStack.delete(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
private collectImports(filePath: string, tree: Parser.Tree): void {
|
||||
const fileImports = new Map<string, ImportInfo>();
|
||||
|
||||
const visit = (node: Parser.SyntaxNode): void => {
|
||||
if (node.type === 'import_declaration') {
|
||||
const source = node.childForFieldName('source')?.text.slice(1, -1) ?? '';
|
||||
const specifiers = node.childForFieldName('specifiers');
|
||||
|
||||
specifiers?.children.forEach(spec => {
|
||||
if (spec.type === 'import_specifier') {
|
||||
const local = spec.childForFieldName('local')?.text ?? '';
|
||||
const imported = spec.childForFieldName('imported')?.text ?? '';
|
||||
fileImports.set(local, { source, imported });
|
||||
}
|
||||
});
|
||||
}
|
||||
node.children.forEach(visit);
|
||||
};
|
||||
|
||||
visit(tree.rootNode);
|
||||
this.imports.set(filePath, fileImports);
|
||||
}
|
||||
|
||||
private collectDefinitions(filePath: string, tree: Parser.Tree): void {
|
||||
const visit = (node: Parser.SyntaxNode): void => {
|
||||
if (node.type === 'function_declaration') {
|
||||
const name = node.childForFieldName('name')?.text ?? '';
|
||||
this.definitions.set(name, { file: filePath, node });
|
||||
}
|
||||
else if (node.type === 'variable_declarator') {
|
||||
const name = node.childForFieldName('name')?.text;
|
||||
const value = node.childForFieldName('value');
|
||||
if (name && (value?.type === 'arrow_function' || value?.type === 'function')) {
|
||||
this.definitions.set(name, { file: filePath, node: value });
|
||||
}
|
||||
}
|
||||
node.children.forEach(visit);
|
||||
};
|
||||
|
||||
visit(tree.rootNode);
|
||||
}
|
||||
|
||||
private async getTypeFromPosition(uri: vscode.Uri, position: vscode.Position): Promise<string | null> {
|
||||
const hover = await vscode.commands.executeCommand<vscode.Hover[]>(
|
||||
'vscode.executeHoverProvider',
|
||||
uri,
|
||||
position
|
||||
);
|
||||
|
||||
if (hover?.[0]?.contents.length) {
|
||||
for (const content of hover[0].contents) {
|
||||
let hoverText = typeof content === 'string' ?
|
||||
content :
|
||||
('value' in content ? content.value : '');
|
||||
|
||||
// Remove typescript backticks if present
|
||||
hoverText = hoverText.replace(/```typescript\s*/, '').replace(/```\s*$/, '');
|
||||
console.log('Processing hover text:', hoverText);
|
||||
|
||||
// Extract the type information - look for the type after the colon
|
||||
const typeMatches = [
|
||||
/:\s*([\w<>]+)(?:\[\])?/, // matches "foo: Type" or "foo: Type[]"
|
||||
/var\s+\w+:\s*([\w<>]+)/, // matches "var foo: Type"
|
||||
/\(type\)\s+[\w<>]+:\s*([\w<>]+)/, // matches "(type) foo: Type"
|
||||
/\(method\)\s*([\w<>]+)\./ // matches "(method) Type.method"
|
||||
];
|
||||
|
||||
for (const pattern of typeMatches) {
|
||||
const match = pattern.exec(hoverText);
|
||||
if (match) {
|
||||
let type = match[1];
|
||||
// Handle array types
|
||||
if (hoverText.includes('[]')) {
|
||||
return 'Array';
|
||||
}
|
||||
// Extract base type from generics
|
||||
if (type.includes('<')) {
|
||||
type = type.split('<')[0];
|
||||
}
|
||||
return type;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private async getCallsInDefn(defnNode: Parser.SyntaxNode, currentFile: string): Promise<Set<string>> {
|
||||
const calls = new Set<string>();
|
||||
const fileImports = this.imports.get(currentFile) ?? new Map();
|
||||
const uri = vscode.Uri.file(currentFile);
|
||||
|
||||
const visit = async (node: Parser.SyntaxNode): Promise<void> => {
|
||||
if (node.type === 'call_expression') {
|
||||
const callee = node.childForFieldName('function');
|
||||
if (callee?.type === 'identifier') {
|
||||
const name = callee.text;
|
||||
const importInfo = fileImports.get(name);
|
||||
if (importInfo) {
|
||||
calls.add(`${importInfo.source}:${importInfo.imported}`);
|
||||
} else {
|
||||
calls.add(name);
|
||||
}
|
||||
}
|
||||
else if (callee?.type === 'member_expression') {
|
||||
const method = callee.childForFieldName('property')?.text;
|
||||
const object = callee.childForFieldName('object');
|
||||
|
||||
if (method && object) {
|
||||
const position = new vscode.Position(
|
||||
object.startPosition.row,
|
||||
object.startPosition.column
|
||||
);
|
||||
|
||||
const type = await this.getTypeFromPosition(uri, position);
|
||||
if (type) {
|
||||
calls.add(`${type}.${method}`);
|
||||
} else {
|
||||
calls.add(`method:${method}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const child of node.children) {
|
||||
await visit(child);
|
||||
}
|
||||
};
|
||||
|
||||
await visit(defnNode);
|
||||
return calls;
|
||||
}
|
||||
|
||||
private gotoDefn(name: string): Definition | null {
|
||||
if (name.includes(':')) {
|
||||
const [file, funcName] = name.split(':');
|
||||
const def = this.definitions.get(funcName);
|
||||
return def ?? null;
|
||||
}
|
||||
|
||||
return this.definitions.get(name) ?? null;
|
||||
}
|
||||
|
||||
private getUses(defnNode: Parser.SyntaxNode, currentFile: string): DefnUse[] {
|
||||
const uses: DefnUse[] = [];
|
||||
|
||||
let fnName: string | undefined;
|
||||
if (defnNode.type === 'function_declaration') {
|
||||
fnName = defnNode.childForFieldName('name')?.text;
|
||||
} else if (defnNode.type === 'arrow_function' || defnNode.type === 'function') {
|
||||
const parent = defnNode.parent;
|
||||
if (parent?.type === 'variable_declarator') {
|
||||
fnName = parent.childForFieldName('name')?.text;
|
||||
}
|
||||
}
|
||||
|
||||
if (!fnName) return uses;
|
||||
|
||||
for (const [file, tree] of this.parsedFiles) {
|
||||
const visit = (node: Parser.SyntaxNode): void => {
|
||||
if (node.type === 'call_expression') {
|
||||
const callee = node.childForFieldName('function');
|
||||
if (callee?.type === 'identifier' && callee.text === fnName) {
|
||||
let current: Parser.SyntaxNode | null = node;
|
||||
while (current) {
|
||||
if (current.type === 'function_declaration' ||
|
||||
current.type === 'arrow_function' ||
|
||||
current.type === 'function') {
|
||||
uses.push({ parent: current, file });
|
||||
break;
|
||||
}
|
||||
current = current.parent;
|
||||
}
|
||||
}
|
||||
}
|
||||
node.children.forEach(visit);
|
||||
};
|
||||
|
||||
visit(tree.rootNode);
|
||||
}
|
||||
|
||||
return uses;
|
||||
}
|
||||
|
||||
private async visitAllNodesInGraphFromDefinition(defn: Parser.SyntaxNode, currentFile: string): Promise<void> {
|
||||
let defnName: string | undefined;
|
||||
if (defn.type === 'function_declaration') {
|
||||
defnName = defn.childForFieldName('name')?.text;
|
||||
} else if (defn.type === 'arrow_function' || defn.type === 'function') {
|
||||
const parent = defn.parent;
|
||||
if (parent?.type === 'variable_declarator') {
|
||||
defnName = parent.childForFieldName('name')?.text;
|
||||
}
|
||||
}
|
||||
|
||||
if (!defnName) return;
|
||||
|
||||
const fullName = `${currentFile}:${defnName}`;
|
||||
if (this.visited.has(fullName)) return;
|
||||
|
||||
const calls = await this.getCallsInDefn(defn, currentFile);
|
||||
this.graph.set(fullName, calls);
|
||||
this.visited.add(fullName);
|
||||
|
||||
const callDefns = Array.from(calls).map(call => this.gotoDefn(call));
|
||||
for (const callDefn of callDefns) {
|
||||
if (callDefn) {
|
||||
await this.visitAllNodesInGraphFromDefinition(callDefn.node, callDefn.file);
|
||||
}
|
||||
}
|
||||
|
||||
const defnUses = this.getUses(defn, currentFile);
|
||||
for (const defnUse of defnUses) {
|
||||
await this.visitAllNodesInGraphFromDefinition(defnUse.parent, defnUse.file);
|
||||
}
|
||||
}
|
||||
|
||||
async analyze(entryFile: string): Promise<Map<string, Set<string>>> {
|
||||
const tree = await this.parseFile(entryFile);
|
||||
if (!tree) return new Map();
|
||||
|
||||
const visit = async (node: Parser.SyntaxNode): Promise<void> => {
|
||||
if (node.type === 'function_declaration') {
|
||||
await this.visitAllNodesInGraphFromDefinition(node, entryFile);
|
||||
}
|
||||
else if (node.type === 'variable_declarator') {
|
||||
const value = node.childForFieldName('value');
|
||||
if (value?.type === 'arrow_function' || value?.type === 'function') {
|
||||
await this.visitAllNodesInGraphFromDefinition(value, entryFile);
|
||||
}
|
||||
}
|
||||
for (const child of node.children) {
|
||||
await visit(child);
|
||||
}
|
||||
};
|
||||
|
||||
await visit(tree.rootNode);
|
||||
return this.graph;
|
||||
}
|
||||
}
|
||||
|
||||
export async function runTreeSitter(filePath?: string): Promise<Map<string, Set<string>> | null> {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (!editor && !filePath) {
|
||||
vscode.window.showWarningMessage('No active editor found');
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const targetPath = filePath ?? editor!.document.uri.fsPath;
|
||||
const analyzer = new ProjectAnalyzer();
|
||||
const graph = await analyzer.analyze(targetPath);
|
||||
|
||||
for (const [defn, calls] of graph) {
|
||||
console.log(`${defn} calls: ${[...calls].join(', ')}`);
|
||||
}
|
||||
|
||||
return graph;
|
||||
} catch (error) {
|
||||
console.error('Error analyzing file:', error);
|
||||
vscode.window.showErrorMessage('Error analyzing file');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
const legend = new vscode.SemanticTokensLegend([], []);
|
||||
|
||||
export async function findFunctions() {
|
||||
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (!editor) return;
|
||||
const document = editor.document;
|
||||
|
||||
const tokens = await vscode.commands.executeCommand<vscode.SemanticTokens>(
|
||||
'vscode.provideDocumentSemanticTokens',
|
||||
document.uri
|
||||
);
|
||||
|
||||
if (!tokens) {
|
||||
console.error('No tokens found');
|
||||
return [];
|
||||
}
|
||||
|
||||
const allTokens = decodeTokens(tokens, document);
|
||||
|
||||
|
||||
return allTokens;
|
||||
}
|
||||
|
||||
function decodeTokens(tokens: vscode.SemanticTokens, document: vscode.TextDocument) {
|
||||
const data = tokens.data;
|
||||
const decodedTokens = [];
|
||||
let line = 0;
|
||||
let character = 0;
|
||||
|
||||
for (let i = 0; i < data.length; i += 5) {
|
||||
const deltaLine = data[i];
|
||||
const deltaStartChar = data[i + 1];
|
||||
const length = data[i + 2];
|
||||
const tokenTypeIdx = data[i + 3];
|
||||
const tokenModifierIdx = data[i + 4];
|
||||
|
||||
line += deltaLine;
|
||||
character = deltaLine === 0 ? character + deltaStartChar : deltaStartChar;
|
||||
|
||||
const type = legend.tokenTypes[tokenTypeIdx] || `(${tokenTypeIdx})`;
|
||||
const modifier = legend.tokenModifiers[tokenModifierIdx] || `(${tokenModifierIdx})`;
|
||||
|
||||
const tokenRange = new vscode.Range(line, character, line, character + length);
|
||||
const tokenText = document.getText(tokenRange);
|
||||
|
||||
decodedTokens.push({
|
||||
line,
|
||||
startCharacter: character,
|
||||
length,
|
||||
type,
|
||||
modifier,
|
||||
text: tokenText,
|
||||
});
|
||||
|
||||
console.log(`Token: '${tokenText}' | Type: ${type} | Modifier: ${modifier} | Line: ${line}, Character: ${character}`);
|
||||
}
|
||||
|
||||
return decodedTokens;
|
||||
}
|
||||
Generated
+775
@@ -156,6 +156,7 @@
|
||||
"mocha": "^10.2.0",
|
||||
"mocha-junit-reporter": "^2.2.1",
|
||||
"mocha-multi-reporters": "^1.5.1",
|
||||
"next": "^15.1.4",
|
||||
"nodemon": "^3.1.9",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"opn": "^6.0.0",
|
||||
@@ -1056,6 +1057,17 @@
|
||||
"node": ">= 4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz",
|
||||
"integrity": "sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@es-joy/jsdoccomment": {
|
||||
"version": "0.41.0",
|
||||
"resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.41.0.tgz",
|
||||
@@ -1702,6 +1714,386 @@
|
||||
"deprecated": "Use @eslint/object-schema instead",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@img/sharp-darwin-arm64": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz",
|
||||
"integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-darwin-arm64": "1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-darwin-x64": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz",
|
||||
"integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-darwin-x64": "1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-darwin-arm64": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz",
|
||||
"integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-darwin-x64": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz",
|
||||
"integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-arm": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz",
|
||||
"integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-arm64": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz",
|
||||
"integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-s390x": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz",
|
||||
"integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-x64": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz",
|
||||
"integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz",
|
||||
"integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz",
|
||||
"integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-arm": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz",
|
||||
"integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-arm": "1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-arm64": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz",
|
||||
"integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-arm64": "1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-s390x": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz",
|
||||
"integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-s390x": "1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-x64": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz",
|
||||
"integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-x64": "1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linuxmusl-arm64": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz",
|
||||
"integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linuxmusl-x64": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz",
|
||||
"integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linuxmusl-x64": "1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-wasm32": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz",
|
||||
"integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==",
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/runtime": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-ia32": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz",
|
||||
"integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-x64": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz",
|
||||
"integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/cliui": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||
@@ -1974,6 +2366,149 @@
|
||||
"exenv-es6": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/env": {
|
||||
"version": "15.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.1.4.tgz",
|
||||
"integrity": "sha512-2fZ5YZjedi5AGaeoaC0B20zGntEHRhi2SdWcu61i48BllODcAmmtj8n7YarSPt4DaTsJaBFdxQAVEVzgmx2Zpw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@next/swc-darwin-arm64": {
|
||||
"version": "15.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.1.4.tgz",
|
||||
"integrity": "sha512-wBEMBs+np+R5ozN1F8Y8d/Dycns2COhRnkxRc+rvnbXke5uZBHkUGFgWxfTXn5rx7OLijuUhyfB+gC/ap58dDw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-darwin-x64": {
|
||||
"version": "15.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.1.4.tgz",
|
||||
"integrity": "sha512-7sgf5rM7Z81V9w48F02Zz6DgEJulavC0jadab4ZsJ+K2sxMNK0/BtF8J8J3CxnsJN3DGcIdC260wEKssKTukUw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-gnu": {
|
||||
"version": "15.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.1.4.tgz",
|
||||
"integrity": "sha512-JaZlIMNaJenfd55kjaLWMfok+vWBlcRxqnRoZrhFQrhM1uAehP3R0+Aoe+bZOogqlZvAz53nY/k3ZyuKDtT2zQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-musl": {
|
||||
"version": "15.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.1.4.tgz",
|
||||
"integrity": "sha512-7EBBjNoyTO2ipMDgCiORpwwOf5tIueFntKjcN3NK+GAQD7OzFJe84p7a2eQUeWdpzZvhVXuAtIen8QcH71ZCOQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-gnu": {
|
||||
"version": "15.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.1.4.tgz",
|
||||
"integrity": "sha512-9TGEgOycqZFuADyFqwmK/9g6S0FYZ3tphR4ebcmCwhL8Y12FW8pIBKJvSwV+UBjMkokstGNH+9F8F031JZKpHw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-musl": {
|
||||
"version": "15.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.1.4.tgz",
|
||||
"integrity": "sha512-0578bLRVDJOh+LdIoKvgNDz77+Bd85c5JrFgnlbI1SM3WmEQvsjxTA8ATu9Z9FCiIS/AliVAW2DV/BDwpXbtiQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-arm64-msvc": {
|
||||
"version": "15.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.1.4.tgz",
|
||||
"integrity": "sha512-JgFCiV4libQavwII+kncMCl30st0JVxpPOtzWcAI2jtum4HjYaclobKhj+JsRu5tFqMtA5CJIa0MvYyuu9xjjQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-x64-msvc": {
|
||||
"version": "15.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.1.4.tgz",
|
||||
"integrity": "sha512-xxsJy9wzq7FR5SqPCUqdgSXiNXrMuidgckBa8nH9HtjjxsilgcN6VgXF6tZ3uEWuVEadotQJI8/9EQ6guTC4Yw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
@@ -2711,6 +3246,23 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/counter": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
|
||||
"integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@swc/helpers": {
|
||||
"version": "0.5.15",
|
||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
|
||||
"integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@szmarczak/http-timer": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz",
|
||||
@@ -5482,6 +6034,18 @@
|
||||
"esbuild": ">=0.18"
|
||||
}
|
||||
},
|
||||
"node_modules/busboy": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
||||
"integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"streamsearch": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.16.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bytes": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||
@@ -5907,6 +6471,13 @@
|
||||
"integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/client-only": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
|
||||
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cliui": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||
@@ -6111,6 +6682,21 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
|
||||
"integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1",
|
||||
"color-string": "^1.9.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
@@ -6129,6 +6715,18 @@
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/color-string": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
|
||||
"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"color-name": "^1.0.0",
|
||||
"simple-swizzle": "^0.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/color-support": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
|
||||
@@ -15620,12 +16218,96 @@
|
||||
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/next": {
|
||||
"version": "15.1.4",
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-15.1.4.tgz",
|
||||
"integrity": "sha512-mTaq9dwaSuwwOrcu3ebjDYObekkxRnXpuVL21zotM8qE2W0HBOdVIdg2Li9QjMEZrj73LN96LcWcz62V19FjAg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@next/env": "15.1.4",
|
||||
"@swc/counter": "0.1.3",
|
||||
"@swc/helpers": "0.5.15",
|
||||
"busboy": "1.6.0",
|
||||
"caniuse-lite": "^1.0.30001579",
|
||||
"postcss": "8.4.31",
|
||||
"styled-jsx": "5.1.6"
|
||||
},
|
||||
"bin": {
|
||||
"next": "dist/bin/next"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^19.8.0 || >= 20.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@next/swc-darwin-arm64": "15.1.4",
|
||||
"@next/swc-darwin-x64": "15.1.4",
|
||||
"@next/swc-linux-arm64-gnu": "15.1.4",
|
||||
"@next/swc-linux-arm64-musl": "15.1.4",
|
||||
"@next/swc-linux-x64-gnu": "15.1.4",
|
||||
"@next/swc-linux-x64-musl": "15.1.4",
|
||||
"@next/swc-win32-arm64-msvc": "15.1.4",
|
||||
"@next/swc-win32-x64-msvc": "15.1.4",
|
||||
"sharp": "^0.33.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": "^1.1.0",
|
||||
"@playwright/test": "^1.41.2",
|
||||
"babel-plugin-react-compiler": "*",
|
||||
"react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0",
|
||||
"react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0",
|
||||
"sass": "^1.3.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@opentelemetry/api": {
|
||||
"optional": true
|
||||
},
|
||||
"@playwright/test": {
|
||||
"optional": true
|
||||
},
|
||||
"babel-plugin-react-compiler": {
|
||||
"optional": true
|
||||
},
|
||||
"sass": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/next-tick": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz",
|
||||
"integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/next/node_modules/postcss": {
|
||||
"version": "8.4.31",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
|
||||
"integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/postcss/"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/postcss"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.6",
|
||||
"picocolors": "^1.0.0",
|
||||
"source-map-js": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/nice-try": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
|
||||
@@ -19444,6 +20126,47 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sharp": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz",
|
||||
"integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"color": "^4.2.3",
|
||||
"detect-libc": "^2.0.3",
|
||||
"semver": "^7.6.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-darwin-arm64": "0.33.5",
|
||||
"@img/sharp-darwin-x64": "0.33.5",
|
||||
"@img/sharp-libvips-darwin-arm64": "1.0.4",
|
||||
"@img/sharp-libvips-darwin-x64": "1.0.4",
|
||||
"@img/sharp-libvips-linux-arm": "1.0.5",
|
||||
"@img/sharp-libvips-linux-arm64": "1.0.4",
|
||||
"@img/sharp-libvips-linux-s390x": "1.0.4",
|
||||
"@img/sharp-libvips-linux-x64": "1.0.4",
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "1.0.4",
|
||||
"@img/sharp-libvips-linuxmusl-x64": "1.0.4",
|
||||
"@img/sharp-linux-arm": "0.33.5",
|
||||
"@img/sharp-linux-arm64": "0.33.5",
|
||||
"@img/sharp-linux-s390x": "0.33.5",
|
||||
"@img/sharp-linux-x64": "0.33.5",
|
||||
"@img/sharp-linuxmusl-arm64": "0.33.5",
|
||||
"@img/sharp-linuxmusl-x64": "0.33.5",
|
||||
"@img/sharp-wasm32": "0.33.5",
|
||||
"@img/sharp-win32-ia32": "0.33.5",
|
||||
"@img/sharp-win32-x64": "0.33.5"
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
@@ -19604,6 +20327,25 @@
|
||||
"simple-concat": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/simple-swizzle": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
|
||||
"integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"is-arrayish": "^0.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/simple-swizzle/node_modules/is-arrayish": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
|
||||
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/simple-update-notifier": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
|
||||
@@ -20254,6 +20996,15 @@
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/streamsearch": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
|
||||
"integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/streamx": {
|
||||
"version": "2.21.1",
|
||||
"resolved": "https://registry.npmjs.org/streamx/-/streamx-2.21.1.tgz",
|
||||
@@ -20492,6 +21243,30 @@
|
||||
"webpack": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/styled-jsx": {
|
||||
"version": "5.1.6",
|
||||
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz",
|
||||
"integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"client-only": "0.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@babel/core": {
|
||||
"optional": true
|
||||
},
|
||||
"babel-plugin-macros": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/stylehacks": {
|
||||
"version": "6.1.1",
|
||||
"resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-6.1.1.tgz",
|
||||
|
||||
@@ -223,6 +223,7 @@
|
||||
"mocha": "^10.2.0",
|
||||
"mocha-junit-reporter": "^2.2.1",
|
||||
"mocha-multi-reporters": "^1.5.1",
|
||||
"next": "^15.1.4",
|
||||
"nodemon": "^3.1.9",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"opn": "^6.0.0",
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<Application xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<VisualElements
|
||||
BackgroundColor="#2D2D30"
|
||||
BackgroundColor="#FFFFFF"
|
||||
ShowNameOnSquare150x150Logo="on"
|
||||
Square150x150Logo="resources\app\resources\win32\code_150x150.png"
|
||||
Square70x70Logo="resources\app\resources\win32\code_70x70.png"
|
||||
ForegroundText="light"
|
||||
ShortDisplayName="Code - OSS" />
|
||||
ForegroundText="light"
|
||||
ShortDisplayName="Void" />
|
||||
</Application>
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 5.5 MiB After Width: | Height: | Size: 1.4 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 795 KiB |
@@ -124,7 +124,8 @@ import { ExtensionSignatureVerificationService, IExtensionSignatureVerificationS
|
||||
import { LLMMessageChannel } from '../../platform/void/electron-main/llmMessageChannel.js';
|
||||
import { IMetricsService } from '../../platform/void/common/metricsService.js';
|
||||
import { MetricsMainService } from '../../platform/void/electron-main/metricsMainService.js';
|
||||
|
||||
import { VoidMainUpdateService } from '../../platform/void/electron-main/voidUpdateMainService.js';
|
||||
import { IVoidUpdateService } from '../../platform/void/common/voidUpdateService.js';
|
||||
/**
|
||||
* The main VS Code application. There will only ever be one instance,
|
||||
* even if the user starts many instances (e.g. from the command line).
|
||||
@@ -1107,6 +1108,7 @@ export class CodeApplication extends Disposable {
|
||||
|
||||
// Void main process services (required for services with a channel for comm between browser and electron-main (node))
|
||||
services.set(IMetricsService, new SyncDescriptor(MetricsMainService, undefined, false));
|
||||
services.set(IVoidUpdateService, new SyncDescriptor(VoidMainUpdateService, undefined, false));
|
||||
|
||||
// Default Extensions Profile Init
|
||||
services.set(IExtensionsProfileScannerService, new SyncDescriptor(ExtensionsProfileScannerService, undefined, true));
|
||||
@@ -1245,6 +1247,10 @@ export class CodeApplication extends Disposable {
|
||||
// Void - use loggerChannel as reference
|
||||
const metricsChannel = ProxyChannel.fromService(accessor.get(IMetricsService), disposables);
|
||||
mainProcessElectronServer.registerChannel('void-channel-metrics', metricsChannel);
|
||||
|
||||
const voidUpdatesChannel = ProxyChannel.fromService(accessor.get(IVoidUpdateService), disposables);
|
||||
mainProcessElectronServer.registerChannel('void-channel-update', voidUpdatesChannel);
|
||||
|
||||
const llmMessageChannel = new LLMMessageChannel(accessor.get(IMetricsService));
|
||||
mainProcessElectronServer.registerChannel('void-channel-llmMessageService', llmMessageChannel);
|
||||
|
||||
|
||||
@@ -223,6 +223,11 @@ export class BaseEditorSimpleWorker implements IDisposable, IWorkerTextModelSync
|
||||
private static readonly _diffLimit = 100000;
|
||||
|
||||
public async $computeMoreMinimalEdits(modelUrl: string, edits: TextEdit[], pretty: boolean): Promise<TextEdit[]> {
|
||||
return this.$Void_computeMoreMinimalEdits(modelUrl, edits, pretty)
|
||||
}
|
||||
|
||||
// Void added this as non async
|
||||
public $Void_computeMoreMinimalEdits(modelUrl: string, edits: TextEdit[], pretty: boolean): TextEdit[] {
|
||||
const model = this._getModel(modelUrl);
|
||||
if (!model) {
|
||||
return edits;
|
||||
|
||||
@@ -403,6 +403,7 @@ function isVersionValid(currentVersion: string, date: ProductDate, requestedVers
|
||||
}
|
||||
|
||||
if (!isValidVersion(currentVersion, date, desiredVersion)) {
|
||||
// Void - ignore not compatible
|
||||
notices.push(nls.localize('versionMismatch', "Extension is not compatible with Code {0}. Extension requires: {1}.", currentVersion, requestedVersion));
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -64,7 +64,8 @@ export const enum KeybindingWeight {
|
||||
EditorContrib = 100,
|
||||
WorkbenchContrib = 200,
|
||||
BuiltinExtension = 300,
|
||||
ExternalExtension = 400
|
||||
ExternalExtension = 400,
|
||||
VoidExtension = 605, // Void - must trump any external extension
|
||||
}
|
||||
|
||||
export interface ICommandAndKeybindingRule extends IKeybindingRule {
|
||||
|
||||
@@ -31,25 +31,29 @@ export class ServerTelemetryService extends TelemetryService implements IServerT
|
||||
}
|
||||
|
||||
override publicLog(eventName: string, data?: ITelemetryData) {
|
||||
if (this._injectedTelemetryLevel < TelemetryLevel.USAGE) {
|
||||
return;
|
||||
}
|
||||
return super.publicLog(eventName, data);
|
||||
// Void commented this out
|
||||
// if (this._injectedTelemetryLevel < TelemetryLevel.USAGE) {
|
||||
// return;
|
||||
// }
|
||||
// return super.publicLog(eventName, data);
|
||||
}
|
||||
|
||||
override publicLog2<E extends ClassifiedEvent<OmitMetadata<T>> = never, T extends IGDPRProperty = never>(eventName: string, data?: StrictPropertyCheck<T, E>) {
|
||||
return this.publicLog(eventName, data as ITelemetryData | undefined);
|
||||
// Void commented this out
|
||||
// return this.publicLog(eventName, data as ITelemetryData | undefined);
|
||||
}
|
||||
|
||||
override publicLogError(errorEventName: string, data?: ITelemetryData) {
|
||||
if (this._injectedTelemetryLevel < TelemetryLevel.ERROR) {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
return super.publicLogError(errorEventName, data);
|
||||
// Void commented this out
|
||||
// if (this._injectedTelemetryLevel < TelemetryLevel.ERROR) {
|
||||
// return Promise.resolve(undefined);
|
||||
// }
|
||||
// return super.publicLogError(errorEventName, data);
|
||||
}
|
||||
|
||||
override publicLogError2<E extends ClassifiedEvent<OmitMetadata<T>> = never, T extends IGDPRProperty = never>(eventName: string, data?: StrictPropertyCheck<T, E>) {
|
||||
return this.publicLogError(eventName, data as ITelemetryData | undefined);
|
||||
// Void commented this out
|
||||
// return this.publicLogError(eventName, data as ITelemetryData | undefined);
|
||||
}
|
||||
|
||||
async updateInjectedTelemetryLevel(telemetryLevel: TelemetryLevel): Promise<void> {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { DisposableStore } from '../../../base/common/lifecycle.js';
|
||||
import { mixin } from '../../../base/common/objects.js';
|
||||
// import { mixin } from '../../../base/common/objects.js';
|
||||
import { isWeb } from '../../../base/common/platform.js';
|
||||
import { escapeRegExpCharacters } from '../../../base/common/strings.js';
|
||||
import { localize } from '../../../nls.js';
|
||||
@@ -15,7 +15,8 @@ import { IProductService } from '../../product/common/productService.js';
|
||||
import { Registry } from '../../registry/common/platform.js';
|
||||
import { ClassifiedEvent, IGDPRProperty, OmitMetadata, StrictPropertyCheck } from './gdprTypings.js';
|
||||
import { ITelemetryData, ITelemetryService, TelemetryConfiguration, TelemetryLevel, TELEMETRY_CRASH_REPORTER_SETTING_ID, TELEMETRY_OLD_SETTING_ID, TELEMETRY_SECTION_ID, TELEMETRY_SETTING_ID, ICommonProperties } from './telemetry.js';
|
||||
import { cleanData, getTelemetryLevel, ITelemetryAppender } from './telemetryUtils.js';
|
||||
import { getTelemetryLevel, ITelemetryAppender } from './telemetryUtils.js';
|
||||
// import { cleanData } from './telemetryUtils.js';
|
||||
|
||||
export interface ITelemetryServiceConfig {
|
||||
appenders: ITelemetryAppender[];
|
||||
@@ -38,7 +39,7 @@ export class TelemetryService implements ITelemetryService {
|
||||
readonly firstSessionDate: string;
|
||||
readonly msftInternal: boolean | undefined;
|
||||
|
||||
private _appenders: ITelemetryAppender[];
|
||||
// private _appenders: ITelemetryAppender[];
|
||||
private _commonProperties: ICommonProperties;
|
||||
private _experimentProperties: { [name: string]: string } = {};
|
||||
private _piiPaths: string[];
|
||||
@@ -53,7 +54,7 @@ export class TelemetryService implements ITelemetryService {
|
||||
@IConfigurationService private _configurationService: IConfigurationService,
|
||||
@IProductService private _productService: IProductService
|
||||
) {
|
||||
this._appenders = config.appenders;
|
||||
// this._appenders = config.appenders;
|
||||
this._commonProperties = config.commonProperties ?? Object.create(null);
|
||||
|
||||
this.sessionId = this._commonProperties['sessionID'] as string;
|
||||
@@ -121,44 +122,47 @@ export class TelemetryService implements ITelemetryService {
|
||||
this._disposables.dispose();
|
||||
}
|
||||
|
||||
private _log(eventName: string, eventLevel: TelemetryLevel, data?: ITelemetryData) {
|
||||
// don't send events when the user is optout
|
||||
if (this._telemetryLevel < eventLevel) {
|
||||
return;
|
||||
}
|
||||
// Void commented this out
|
||||
// private _log(eventName: string, eventLevel: TelemetryLevel, data?: ITelemetryData) {
|
||||
// // don't send events when the user is optout
|
||||
// if (this._telemetryLevel < eventLevel) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// add experiment properties
|
||||
data = mixin(data, this._experimentProperties);
|
||||
// // add experiment properties
|
||||
// data = mixin(data, this._experimentProperties);
|
||||
|
||||
// remove all PII from data
|
||||
data = cleanData(data as Record<string, any>, this._cleanupPatterns);
|
||||
// // remove all PII from data
|
||||
// data = cleanData(data as Record<string, any>, this._cleanupPatterns);
|
||||
|
||||
// add common properties
|
||||
data = mixin(data, this._commonProperties);
|
||||
// // add common properties
|
||||
// data = mixin(data, this._commonProperties);
|
||||
|
||||
// Log to the appenders of sufficient level
|
||||
this._appenders.forEach(a => a.log(eventName, data));
|
||||
}
|
||||
// // Log to the appenders of sufficient level
|
||||
// this._appenders.forEach(a => a.log(eventName, data));
|
||||
// }
|
||||
|
||||
publicLog(eventName: string, data?: ITelemetryData) {
|
||||
this._log(eventName, TelemetryLevel.USAGE, data);
|
||||
// this._log(eventName, TelemetryLevel.USAGE, data);
|
||||
}
|
||||
|
||||
publicLog2<E extends ClassifiedEvent<OmitMetadata<T>> = never, T extends IGDPRProperty = never>(eventName: string, data?: StrictPropertyCheck<T, E>) {
|
||||
this.publicLog(eventName, data as ITelemetryData);
|
||||
// this.publicLog(eventName, data as ITelemetryData);
|
||||
}
|
||||
|
||||
publicLogError(errorEventName: string, data?: ITelemetryData) {
|
||||
if (!this._sendErrorTelemetry) {
|
||||
return;
|
||||
}
|
||||
// Void commented this out
|
||||
// if (!this._sendErrorTelemetry) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// Send error event and anonymize paths
|
||||
this._log(errorEventName, TelemetryLevel.ERROR, data);
|
||||
// // Send error event and anonymize paths
|
||||
// this._log(errorEventName, TelemetryLevel.ERROR, data);
|
||||
}
|
||||
|
||||
publicLogError2<E extends ClassifiedEvent<OmitMetadata<T>> = never, T extends IGDPRProperty = never>(eventName: string, data?: StrictPropertyCheck<T, E>) {
|
||||
this.publicLogError(eventName, data as ITelemetryData);
|
||||
// Void commented this out
|
||||
// this.publicLogError(eventName, data as ITelemetryData);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,11 +3,12 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
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';
|
||||
import { IEnvironmentMainService } from '../../environment/electron-main/environmentMainService.js';
|
||||
// import { IEnvironmentMainService } from '../../environment/electron-main/environmentMainService.js';
|
||||
import { ILifecycleMainService, LifecycleMainPhase } from '../../lifecycle/electron-main/lifecycleMainService.js';
|
||||
import { ILogService } from '../../log/common/log.js';
|
||||
import { IProductService } from '../../product/common/productService.js';
|
||||
@@ -15,7 +16,10 @@ import { IRequestService } from '../../request/common/request.js';
|
||||
import { AvailableForDownload, DisablementReason, IUpdateService, State, StateType, UpdateType } from '../common/update.js';
|
||||
|
||||
export function createUpdateURL(platform: string, quality: string, productService: IProductService): string {
|
||||
return `${productService.updateUrl}/api/update/${platform}/${quality}/${productService.commit}`;
|
||||
// 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}`;
|
||||
}
|
||||
|
||||
export type UpdateNotAvailableClassification = {
|
||||
@@ -57,7 +61,7 @@ export abstract class AbstractUpdateService implements IUpdateService {
|
||||
@IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService,
|
||||
@IRequestService protected requestService: IRequestService,
|
||||
@ILogService protected logService: ILogService,
|
||||
@IProductService protected readonly productService: IProductService
|
||||
@IProductService protected readonly productService: IProductService,
|
||||
) {
|
||||
lifecycleMainService.when(LifecycleMainPhase.AfterWindowOpen)
|
||||
.finally(() => this.initialize());
|
||||
@@ -70,75 +74,35 @@ export abstract class AbstractUpdateService implements IUpdateService {
|
||||
*/
|
||||
protected async initialize(): Promise<void> {
|
||||
if (!this.environmentMainService.isBuilt) {
|
||||
console.log('is NOT built, canceling update service')
|
||||
this.setState(State.Disabled(DisablementReason.NotBuilt));
|
||||
return; // updates are never enabled when running out of sources
|
||||
}
|
||||
console.log('is built, continuing with update service')
|
||||
|
||||
if (this.environmentMainService.disableUpdates) {
|
||||
this.setState(State.Disabled(DisablementReason.DisabledByEnvironment));
|
||||
this.logService.info('update#ctor - updates are disabled by the environment');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.productService.updateUrl || !this.productService.commit) {
|
||||
this.setState(State.Disabled(DisablementReason.MissingConfiguration));
|
||||
this.logService.info('update#ctor - updates are disabled as there is no update URL');
|
||||
return;
|
||||
}
|
||||
|
||||
const updateMode = this.configurationService.getValue<'none' | 'manual' | 'start' | 'default'>('update.mode');
|
||||
const quality = this.getProductQuality(updateMode);
|
||||
|
||||
if (!quality) {
|
||||
this.setState(State.Disabled(DisablementReason.ManuallyDisabled));
|
||||
this.logService.info('update#ctor - updates are disabled by user preference');
|
||||
return;
|
||||
}
|
||||
|
||||
this.url = this.buildUpdateFeedUrl(quality);
|
||||
this.url = this.doBuildUpdateFeedUrl('stable');
|
||||
if (!this.url) {
|
||||
this.setState(State.Disabled(DisablementReason.InvalidConfiguration));
|
||||
this.logService.info('update#ctor - updates are disabled as the update URL is badly formed');
|
||||
return;
|
||||
}
|
||||
|
||||
// hidden setting
|
||||
if (this.configurationService.getValue<boolean>('_update.prss')) {
|
||||
const url = new URL(this.url);
|
||||
url.searchParams.set('prss', 'true');
|
||||
this.url = url.toString();
|
||||
}
|
||||
this.setState(State.Disabled(DisablementReason.ManuallyDisabled));
|
||||
|
||||
this.setState(State.Idle(this.getUpdateType()));
|
||||
|
||||
if (updateMode === 'manual') {
|
||||
this.logService.info('update#ctor - manual checks only; automatic updates are disabled by user preference');
|
||||
return;
|
||||
}
|
||||
// Void - temporarily disabled while we figure out how to do this the right way
|
||||
|
||||
if (updateMode === 'start') {
|
||||
this.logService.info('update#ctor - startup checks only; automatic updates are disabled by user preference');
|
||||
// this.setState(State.Idle(this.getUpdateType()));
|
||||
|
||||
// Check for updates only once after 30 seconds
|
||||
setTimeout(() => this.checkForUpdates(false), 30 * 1000);
|
||||
} else {
|
||||
// Start checking for updates after 30 seconds
|
||||
this.scheduleCheckForUpdates(30 * 1000).then(undefined, err => this.logService.error(err));
|
||||
}
|
||||
// start checking for updates after 10 seconds
|
||||
// this.scheduleCheckForUpdates(10 * 1000).then(undefined, err => this.logService.error(err));
|
||||
}
|
||||
|
||||
private getProductQuality(updateMode: string): string | undefined {
|
||||
return updateMode === 'none' ? undefined : this.productService.quality;
|
||||
}
|
||||
|
||||
private scheduleCheckForUpdates(delay = 60 * 60 * 1000): Promise<void> {
|
||||
return timeout(delay)
|
||||
.then(() => this.checkForUpdates(false))
|
||||
.then(() => {
|
||||
// Check again after 1 hour
|
||||
return 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);
|
||||
@@ -160,6 +124,7 @@ export abstract class AbstractUpdateService implements IUpdateService {
|
||||
await this.doDownloadUpdate(this.state);
|
||||
}
|
||||
|
||||
// override implemented by windows and linux
|
||||
protected async doDownloadUpdate(state: AvailableForDownload): Promise<void> {
|
||||
// noop
|
||||
}
|
||||
@@ -174,6 +139,7 @@ export abstract class AbstractUpdateService implements IUpdateService {
|
||||
await this.doApplyUpdate();
|
||||
}
|
||||
|
||||
// windows overrides this
|
||||
protected async doApplyUpdate(): Promise<void> {
|
||||
// noop
|
||||
}
|
||||
@@ -236,6 +202,6 @@ export abstract class AbstractUpdateService implements IUpdateService {
|
||||
// noop
|
||||
}
|
||||
|
||||
protected abstract buildUpdateFeedUrl(quality: string): string | undefined;
|
||||
protected abstract doBuildUpdateFeedUrl(quality: string): string | undefined;
|
||||
protected abstract doCheckForUpdates(context: any): void;
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau
|
||||
@IEnvironmentMainService environmentMainService: IEnvironmentMainService,
|
||||
@IRequestService requestService: IRequestService,
|
||||
@ILogService logService: ILogService,
|
||||
@IProductService productService: IProductService
|
||||
@IProductService productService: IProductService,
|
||||
) {
|
||||
super(lifecycleMainService, configurationService, environmentMainService, requestService, logService, productService);
|
||||
|
||||
@@ -73,7 +73,7 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau
|
||||
this.setState(State.Idle(UpdateType.Archive, message));
|
||||
}
|
||||
|
||||
protected buildUpdateFeedUrl(quality: string): string | undefined {
|
||||
protected doBuildUpdateFeedUrl(quality: string): string | undefined {
|
||||
let assetID: string;
|
||||
if (!this.productService.darwinUniversalAssetId) {
|
||||
assetID = process.arch === 'x64' ? 'darwin' : 'darwin-arm64';
|
||||
|
||||
@@ -30,7 +30,7 @@ export class LinuxUpdateService extends AbstractUpdateService {
|
||||
super(lifecycleMainService, configurationService, environmentMainService, requestService, logService, productService);
|
||||
}
|
||||
|
||||
protected buildUpdateFeedUrl(quality: string): string {
|
||||
protected doBuildUpdateFeedUrl(quality: string): string {
|
||||
return createUpdateURL(`linux-${process.arch}`, quality, this.productService);
|
||||
}
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun
|
||||
@ILogService logService: ILogService,
|
||||
@IFileService private readonly fileService: IFileService,
|
||||
@INativeHostMainService private readonly nativeHostMainService: INativeHostMainService,
|
||||
@IProductService productService: IProductService
|
||||
@IProductService productService: IProductService,
|
||||
) {
|
||||
super(lifecycleMainService, configurationService, environmentMainService, requestService, logService, productService);
|
||||
|
||||
@@ -99,7 +99,7 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun
|
||||
await super.initialize();
|
||||
}
|
||||
|
||||
protected buildUpdateFeedUrl(quality: string): string | undefined {
|
||||
protected doBuildUpdateFeedUrl(quality: string): string | undefined {
|
||||
let platform = `win32-${process.arch}`;
|
||||
|
||||
if (getUpdateType() === UpdateType.Archive) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*------------------------------------------------------------------------------------------
|
||||
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
|
||||
*-----------------------------------------------------------------------------------------*/
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
// ---------- common ----------
|
||||
|
||||
@@ -16,3 +16,6 @@ import '../common/refreshModelService.js'
|
||||
|
||||
// metrics
|
||||
import '../common/metricsService.js'
|
||||
|
||||
// updates
|
||||
import '../common/voidUpdateService.js'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*------------------------------------------------------------------------------------------
|
||||
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
|
||||
*-----------------------------------------------------------------------------------------*/
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, ServiceSendLLMMessageParams, MainLLMMessageParams, MainLLMMessageAbortParams, ServiceModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, MainModelListParams, OllamaModelResponse, OpenaiCompatibleModelResponse, } from './llmMessageTypes.js';
|
||||
import { IChannel } from '../../../base/parts/ipc/common/ipc.js';
|
||||
@@ -52,6 +52,7 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
|
||||
super()
|
||||
|
||||
// const service = ProxyChannel.toService<LLMMessageChannel>(mainProcessService.getChannel('void-channel-sendLLMMessage')); // lets you call it like a service
|
||||
// see llmMessageChannel.ts
|
||||
this.channel = this.mainProcessService.getChannel('void-channel-llmMessageService')
|
||||
|
||||
// .listen sets up an IPC channel and takes a few ms, so we set up listeners immediately and add hooks to them instead
|
||||
@@ -64,18 +65,18 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
|
||||
this._onRequestIdDone(e.requestId)
|
||||
}))
|
||||
this._register((this.channel.listen('onError_llm') satisfies Event<EventLLMMessageOnErrorParams>)(e => {
|
||||
console.log('Error in LLMMessageService:', JSON.stringify(e))
|
||||
console.error('Error in LLMMessageService:', JSON.stringify(e))
|
||||
this.onErrorHooks_llm[e.requestId]?.(e)
|
||||
this._onRequestIdDone(e.requestId)
|
||||
}))
|
||||
// ollama
|
||||
// ollama .list()
|
||||
this._register((this.channel.listen('onSuccess_ollama') satisfies Event<EventModelListOnSuccessParams<OllamaModelResponse>>)(e => {
|
||||
this.onSuccess_ollama[e.requestId]?.(e)
|
||||
}))
|
||||
this._register((this.channel.listen('onError_ollama') satisfies Event<EventModelListOnErrorParams<OllamaModelResponse>>)(e => {
|
||||
this.onError_ollama[e.requestId]?.(e)
|
||||
}))
|
||||
// openaiCompatible
|
||||
// openaiCompatible .list()
|
||||
this._register((this.channel.listen('onSuccess_openAICompatible') satisfies Event<EventModelListOnSuccessParams<OpenaiCompatibleModelResponse>>)(e => {
|
||||
this.onSuccess_openAICompatible[e.requestId]?.(e)
|
||||
}))
|
||||
@@ -87,7 +88,7 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
|
||||
|
||||
sendLLMMessage(params: ServiceSendLLMMessageParams) {
|
||||
const { onText, onFinalMessage, onError, ...proxyParams } = params;
|
||||
const { featureName } = proxyParams
|
||||
const { useProviderFor: featureName } = proxyParams
|
||||
|
||||
// end early if no provider
|
||||
const modelSelection = this.voidSettingsService.state.modelSelectionOfFeature[featureName]
|
||||
@@ -97,6 +98,10 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
|
||||
}
|
||||
const { providerName, modelName } = modelSelection
|
||||
|
||||
const aiInstructions = this.voidSettingsService.state.globalSettings.aiInstructions
|
||||
if (aiInstructions)
|
||||
proxyParams.messages.unshift({ role: 'system', content: aiInstructions })
|
||||
|
||||
// add state for request id
|
||||
const requestId_ = generateUuid();
|
||||
this.onTextHooks_llm[requestId_] = onText
|
||||
|
||||
@@ -1,12 +1,25 @@
|
||||
/*------------------------------------------------------------------------------------------
|
||||
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
|
||||
*-----------------------------------------------------------------------------------------*/
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IRange } from '../../../editor/common/core/range'
|
||||
import { ProviderName, SettingsOfProvider } from './voidSettingsTypes.js'
|
||||
|
||||
|
||||
export const errorDetails = (fullError: Error | null): string | null => {
|
||||
if (fullError === null) {
|
||||
return null
|
||||
}
|
||||
else if (typeof fullError === 'object') {
|
||||
return JSON.stringify(fullError, null, 2)
|
||||
}
|
||||
else if (typeof fullError === 'string') {
|
||||
return null
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export type OnText = (p: { newText: string, fullText: string }) => void
|
||||
export type OnFinalMessage = (p: { fullText: string }) => void
|
||||
export type OnError = (p: { message: string, fullError: Error | null }) => void
|
||||
@@ -18,12 +31,12 @@ export type LLMMessage = {
|
||||
}
|
||||
|
||||
export type ServiceSendLLMFeatureParams = {
|
||||
featureName: 'Ctrl+K';
|
||||
useProviderFor: 'Ctrl+K';
|
||||
range: IRange;
|
||||
} | {
|
||||
featureName: 'Ctrl+L';
|
||||
useProviderFor: 'Ctrl+L';
|
||||
} | {
|
||||
featureName: 'Autocomplete';
|
||||
useProviderFor: 'Autocomplete';
|
||||
range: IRange;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
/*------------------------------------------------------------------------------------------
|
||||
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
|
||||
*-----------------------------------------------------------------------------------------*/
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { createDecorator } from '../../instantiation/common/instantiation.js';
|
||||
import { ProxyChannel } from '../../../base/parts/ipc/common/ipc.js';
|
||||
import { IMainProcessService } from '../../ipc/common/mainProcessService.js';
|
||||
import { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js';
|
||||
import { Action2, registerAction2 } from '../../actions/common/actions.js';
|
||||
import { localize2 } from '../../../nls.js';
|
||||
import { ServicesAccessor } from '../../../editor/browser/editorExtensions.js';
|
||||
import { INotificationService } from '../../notification/common/notification.js';
|
||||
|
||||
export interface IMetricsService {
|
||||
readonly _serviceBrand: undefined;
|
||||
capture(event: string, params: Record<string, any>): void;
|
||||
getDebuggingProperties(): Promise<object>;
|
||||
}
|
||||
|
||||
export const IMetricsService = createDecorator<IMetricsService>('metricsService');
|
||||
@@ -25,6 +30,7 @@ export class MetricsService implements IMetricsService {
|
||||
constructor(
|
||||
@IMainProcessService mainProcessService: IMainProcessService // (only usable on client side)
|
||||
) {
|
||||
// creates an IPC proxy to use metricsMainService.ts
|
||||
this.metricsService = ProxyChannel.toService<IMetricsService>(mainProcessService.getChannel('void-channel-metrics'));
|
||||
}
|
||||
|
||||
@@ -33,7 +39,30 @@ export class MetricsService implements IMetricsService {
|
||||
this.metricsService.capture(...params);
|
||||
}
|
||||
|
||||
// anything transmitted over a channel must be async even if it looks like it doesn't have to be
|
||||
async getDebuggingProperties(): Promise<object> {
|
||||
return this.metricsService.getDebuggingProperties()
|
||||
}
|
||||
}
|
||||
|
||||
registerSingleton(IMetricsService, MetricsService, InstantiationType.Eager);
|
||||
|
||||
|
||||
// debugging action
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'voidDebugInfo',
|
||||
f1: true,
|
||||
title: localize2('voidMetricsDebug', 'Void: Log Debug Info'),
|
||||
});
|
||||
}
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const metricsService = accessor.get(IMetricsService)
|
||||
const notifService = accessor.get(INotificationService)
|
||||
|
||||
const debugProperties = await metricsService.getDebuggingProperties()
|
||||
console.log('Metrics:', debugProperties)
|
||||
notifService.info(`Void Debug info:\n${JSON.stringify(debugProperties, null, 2)}`)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*------------------------------------------------------------------------------------------
|
||||
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
|
||||
*-----------------------------------------------------------------------------------------*/
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { createDecorator } from '../../instantiation/common/instantiation.js';
|
||||
import { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js';
|
||||
@@ -25,22 +25,33 @@ type RefreshableState = ({
|
||||
state: 'finished',
|
||||
timeoutId: null,
|
||||
} | {
|
||||
state: 'finished_invisible',
|
||||
state: 'error',
|
||||
timeoutId: null,
|
||||
})
|
||||
|
||||
|
||||
/*
|
||||
|
||||
user click -> error -> fire(error)
|
||||
\> success -> fire(success)
|
||||
finally: keep polling
|
||||
|
||||
poll -> do not fire
|
||||
|
||||
*/
|
||||
export type RefreshModelStateOfProvider = Record<RefreshableProviderName, RefreshableState>
|
||||
|
||||
|
||||
|
||||
const refreshBasedOn: { [k in RefreshableProviderName]: (keyof SettingsOfProvider[k])[] } = {
|
||||
ollama: ['_enabled', 'endpoint'],
|
||||
openAICompatible: ['_enabled', 'endpoint', 'apiKey'],
|
||||
// openAICompatible: ['_enabled', 'endpoint', 'apiKey'],
|
||||
}
|
||||
const REFRESH_INTERVAL = 5_000
|
||||
// const COOLDOWN_TIMEOUT = 300
|
||||
|
||||
const autoOptions = { enableProviderOnSuccess: true, doNotFire: true }
|
||||
|
||||
// element-wise equals
|
||||
function eq<T>(a: T[], b: T[]): boolean {
|
||||
if (a.length !== b.length) return false
|
||||
@@ -51,7 +62,7 @@ function eq<T>(a: T[], b: T[]): boolean {
|
||||
}
|
||||
export interface IRefreshModelService {
|
||||
readonly _serviceBrand: undefined;
|
||||
refreshModels: (providerName: RefreshableProviderName) => Promise<void>;
|
||||
startRefreshingModels: (providerName: RefreshableProviderName, options: { enableProviderOnSuccess: boolean, doNotFire: boolean }) => void;
|
||||
onDidChangeState: Event<RefreshableProviderName>;
|
||||
state: RefreshModelStateOfProvider;
|
||||
}
|
||||
@@ -75,23 +86,23 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
|
||||
|
||||
const disposables: Set<IDisposable> = new Set()
|
||||
|
||||
const initializePollingAndOnChange = () => {
|
||||
const initializeAutoPollingAndOnChange = () => {
|
||||
this._clearAllTimeouts()
|
||||
disposables.forEach(d => d.dispose())
|
||||
disposables.clear()
|
||||
|
||||
if (!voidSettingsService.state.featureFlagSettings.autoRefreshModels) return
|
||||
if (!voidSettingsService.state.globalSettings.autoRefreshModels) return
|
||||
|
||||
for (const providerName of refreshableProviderNames) {
|
||||
|
||||
const { _enabled: enabled } = this.voidSettingsService.state.settingsOfProvider[providerName]
|
||||
this.refreshModels(providerName, !enabled, { isPolling: true, isInternal: true })
|
||||
// const { _enabled: enabled } = this.voidSettingsService.state.settingsOfProvider[providerName]
|
||||
this.startRefreshingModels(providerName, autoOptions)
|
||||
|
||||
// every time providerName.enabled changes, refresh models too, like a useEffect
|
||||
let relevantVals = () => refreshBasedOn[providerName].map(settingName => this.voidSettingsService.state.settingsOfProvider[providerName][settingName])
|
||||
let relevantVals = () => refreshBasedOn[providerName].map(settingName => voidSettingsService.state.settingsOfProvider[providerName][settingName])
|
||||
let prevVals = relevantVals() // each iteration of a for loop has its own context and vars, so this is ok
|
||||
disposables.add(
|
||||
this.voidSettingsService.onDidChangeState(() => { // we might want to debounce this
|
||||
voidSettingsService.onDidChangeState(() => { // we might want to debounce this
|
||||
const newVals = relevantVals()
|
||||
if (!eq(prevVals, newVals)) {
|
||||
|
||||
@@ -101,7 +112,7 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
|
||||
// if it was just enabled, or there was a change and it wasn't to the enabled state, refresh
|
||||
if ((enabled && !prevEnabled) || (!enabled && !prevEnabled)) {
|
||||
// if user just clicked enable, refresh
|
||||
this.refreshModels(providerName, !enabled, { isPolling: false, isInternal: true })
|
||||
this.startRefreshingModels(providerName, autoOptions)
|
||||
}
|
||||
else {
|
||||
// else if user just clicked disable, don't refresh
|
||||
@@ -117,11 +128,11 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
|
||||
}
|
||||
}
|
||||
|
||||
// on mount (when get init settings state), and if a relevant feature flag changes (detected natively right now by refreshing if any flag changes), start refreshing models
|
||||
// on mount (when get init settings state), and if a relevant feature flag changes, start refreshing models
|
||||
voidSettingsService.waitForInitState.then(() => {
|
||||
initializePollingAndOnChange()
|
||||
initializeAutoPollingAndOnChange()
|
||||
this._register(
|
||||
voidSettingsService.onDidChangeState((type) => { if (type === 'featureFlagSettings') initializePollingAndOnChange() })
|
||||
voidSettingsService.onDidChangeState((type) => { if (typeof type === 'object' && type[1] === 'autoRefreshModels') initializeAutoPollingAndOnChange() })
|
||||
)
|
||||
})
|
||||
|
||||
@@ -129,54 +140,53 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
|
||||
|
||||
state: RefreshModelStateOfProvider = {
|
||||
ollama: { state: 'init', timeoutId: null },
|
||||
openAICompatible: { state: 'init', timeoutId: null },
|
||||
}
|
||||
|
||||
|
||||
// start listening for models (and don't stop until success)
|
||||
async refreshModels(providerName: RefreshableProviderName, enableProviderOnSuccess?: boolean, options?: { isPolling?: boolean, isInternal?: boolean }) {
|
||||
|
||||
const { isPolling, isInternal } = options ?? {}
|
||||
|
||||
console.log(`refreshModels, isInternal ${isInternal} isPolling ${isPolling}`)
|
||||
startRefreshingModels: IRefreshModelService['startRefreshingModels'] = (providerName, options) => {
|
||||
|
||||
this._clearProviderTimeout(providerName)
|
||||
|
||||
// start loading models
|
||||
if (!isInternal) this._setRefreshState(providerName, 'refreshing')
|
||||
this._setRefreshState(providerName, 'refreshing', options)
|
||||
|
||||
const fn = providerName === 'ollama' ? this.llmMessageService.ollamaList
|
||||
const autoPoll = () => {
|
||||
if (this.voidSettingsService.state.globalSettings.autoRefreshModels) {
|
||||
// resume auto-polling
|
||||
const timeoutId = setTimeout(() => this.startRefreshingModels(providerName, autoOptions), REFRESH_INTERVAL)
|
||||
this._setTimeoutId(providerName, timeoutId)
|
||||
}
|
||||
}
|
||||
const listFn = providerName === 'ollama' ? this.llmMessageService.ollamaList
|
||||
: providerName === 'openAICompatible' ? this.llmMessageService.openAICompatibleList
|
||||
: () => { }
|
||||
|
||||
fn({
|
||||
listFn({
|
||||
onSuccess: ({ models }) => {
|
||||
this.voidSettingsService.setDefaultModels(providerName, models.map(model => {
|
||||
if (providerName === 'ollama') return (model as OllamaModelResponse).name
|
||||
else if (providerName === 'openAICompatible') return (model as OpenaiCompatibleModelResponse).id
|
||||
else throw new Error('refreshMode fn: unknown provider', providerName)
|
||||
}))
|
||||
|
||||
if (enableProviderOnSuccess) {
|
||||
this.voidSettingsService.setSettingOfProvider(providerName, '_enabled', true)
|
||||
}
|
||||
// set the models to the detected models
|
||||
this.voidSettingsService.setAutodetectedModels(
|
||||
providerName,
|
||||
models.map(model => {
|
||||
if (providerName === 'ollama') return (model as OllamaModelResponse).name;
|
||||
else if (providerName === 'openAICompatible') return (model as OpenaiCompatibleModelResponse).id;
|
||||
else throw new Error('refreshMode fn: unknown provider', providerName);
|
||||
}),
|
||||
{ enableProviderOnSuccess: options.enableProviderOnSuccess, hideRefresh: options.doNotFire }
|
||||
)
|
||||
|
||||
if (!isInternal) this._setRefreshState(providerName, 'finished')
|
||||
if (options.enableProviderOnSuccess) this.voidSettingsService.setSettingOfProvider(providerName, '_enabled', true)
|
||||
|
||||
this._setRefreshState(providerName, 'finished', options)
|
||||
autoPoll()
|
||||
},
|
||||
onError: ({ error }) => {
|
||||
console.log('retrying list models:', providerName, error)
|
||||
this._setRefreshState(providerName, 'error', options)
|
||||
autoPoll()
|
||||
}
|
||||
})
|
||||
|
||||
if (isInternal) this._setRefreshState(providerName, 'finished_invisible')
|
||||
|
||||
// check if we should poll
|
||||
// if it was originally called as `isPolling` and if the `autoRefreshModels` flag is enabled
|
||||
if (isPolling && this.voidSettingsService.state.featureFlagSettings.autoRefreshModels) {
|
||||
const timeoutId = setTimeout(() => this.refreshModels(providerName, enableProviderOnSuccess, options), REFRESH_INTERVAL)
|
||||
this._setTimeoutId(providerName, timeoutId)
|
||||
}
|
||||
}
|
||||
|
||||
_clearAllTimeouts() {
|
||||
@@ -197,7 +207,8 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
|
||||
this.state[providerName].timeoutId = timeoutId
|
||||
}
|
||||
|
||||
private _setRefreshState(providerName: RefreshableProviderName, state: RefreshableState['state']) {
|
||||
private _setRefreshState(providerName: RefreshableProviderName, state: RefreshableState['state'], options?: { doNotFire: boolean }) {
|
||||
if (options?.doNotFire) return
|
||||
this.state[providerName].state = state
|
||||
this._onDidChangeState.fire(providerName)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*------------------------------------------------------------------------------------------
|
||||
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
|
||||
*-----------------------------------------------------------------------------------------*/
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Emitter, Event } from '../../../base/common/event.js';
|
||||
import { Disposable } from '../../../base/common/lifecycle.js';
|
||||
@@ -10,10 +10,11 @@ import { IEncryptionService } from '../../encryption/common/encryptionService.js
|
||||
import { registerSingleton, InstantiationType } from '../../instantiation/common/extensions.js';
|
||||
import { createDecorator } from '../../instantiation/common/instantiation.js';
|
||||
import { IStorageService, StorageScope, StorageTarget } from '../../storage/common/storage.js';
|
||||
import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, modelInfoOfDefaultNames, VoidModelInfo, FeatureFlagSettings, FeatureFlagName, defaultFeatureFlagSettings } from './voidSettingsTypes.js';
|
||||
import { IMetricsService } from './metricsService.js';
|
||||
import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, modelInfoOfDefaultNames, VoidModelInfo, GlobalSettings, GlobalSettingName, defaultGlobalSettings } from './voidSettingsTypes.js';
|
||||
|
||||
|
||||
const STORAGE_KEY = 'void.voidSettingsStorage'
|
||||
const STORAGE_KEY = 'void.settingsServiceStorage'
|
||||
|
||||
type SetSettingOfProviderFn = <S extends SettingName>(
|
||||
providerName: ProviderName,
|
||||
@@ -27,21 +28,22 @@ type SetModelSelectionOfFeatureFn = <K extends FeatureName>(
|
||||
options?: { doNotApplyEffects?: true }
|
||||
) => Promise<void>;
|
||||
|
||||
type SetFeatureFlagFn = (flagName: FeatureFlagName, newVal: boolean) => void;
|
||||
type SetGlobalSettingFn = <T extends GlobalSettingName, >(settingName: T, newVal: GlobalSettings[T]) => void;
|
||||
|
||||
export type ModelOption = { text: string, value: ModelSelection }
|
||||
export type ModelOption = { name: string, selection: ModelSelection }
|
||||
|
||||
|
||||
|
||||
export type VoidSettingsState = {
|
||||
readonly settingsOfProvider: SettingsOfProvider; // optionsOfProvider
|
||||
readonly modelSelectionOfFeature: ModelSelectionOfFeature; // stateOfFeature
|
||||
readonly featureFlagSettings: FeatureFlagSettings;
|
||||
readonly globalSettings: GlobalSettings;
|
||||
|
||||
readonly _modelOptions: ModelOption[] // computed based on the two above items
|
||||
}
|
||||
|
||||
type EventProp = Exclude<keyof VoidSettingsState, '_modelOptions'> | 'all'
|
||||
type RealVoidSettings = Exclude<keyof VoidSettingsState, '_modelOptions'>
|
||||
type EventProp<T extends RealVoidSettings = RealVoidSettings> = T extends 'globalSettings' ? [T, keyof VoidSettingsState[T]] : T | 'all'
|
||||
|
||||
|
||||
export interface IVoidSettingsService {
|
||||
@@ -53,9 +55,9 @@ export interface IVoidSettingsService {
|
||||
|
||||
setSettingOfProvider: SetSettingOfProviderFn;
|
||||
setModelSelectionOfFeature: SetModelSelectionOfFeatureFn;
|
||||
setFeatureFlag: SetFeatureFlagFn;
|
||||
setGlobalSetting: SetGlobalSettingFn;
|
||||
|
||||
setDefaultModels(providerName: ProviderName, modelNames: string[]): void;
|
||||
setAutodetectedModels(providerName: ProviderName, modelNames: string[], logging: object): void;
|
||||
toggleModelHidden(providerName: ProviderName, modelName: string): void;
|
||||
addModel(providerName: ProviderName, modelName: string): void;
|
||||
deleteModel(providerName: ProviderName, modelName: string): boolean;
|
||||
@@ -69,7 +71,7 @@ let _computeModelOptions = (settingsOfProvider: SettingsOfProvider) => {
|
||||
if (!providerConfig._enabled) continue // if disabled, don't display model options
|
||||
for (const { modelName, isHidden } of providerConfig.models) {
|
||||
if (isHidden) continue
|
||||
modelOptions.push({ text: `${modelName} (${providerName})`, value: { providerName, modelName } })
|
||||
modelOptions.push({ name: `${modelName} (${providerName})`, selection: { providerName, modelName } })
|
||||
}
|
||||
}
|
||||
return modelOptions
|
||||
@@ -80,7 +82,7 @@ const defaultState = () => {
|
||||
const d: VoidSettingsState = {
|
||||
settingsOfProvider: deepClone(defaultSettingsOfProvider),
|
||||
modelSelectionOfFeature: { 'Ctrl+L': null, 'Ctrl+K': null, 'Autocomplete': null },
|
||||
featureFlagSettings: deepClone(defaultFeatureFlagSettings),
|
||||
globalSettings: deepClone(defaultGlobalSettings),
|
||||
_modelOptions: _computeModelOptions(defaultSettingsOfProvider), // computed
|
||||
}
|
||||
return d
|
||||
@@ -100,6 +102,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
||||
constructor(
|
||||
@IStorageService private readonly _storageService: IStorageService,
|
||||
@IEncryptionService private readonly _encryptionService: IEncryptionService,
|
||||
@IMetricsService private readonly _metricsService: IMetricsService,
|
||||
// could have used this, but it's clearer the way it is (+ slightly different eg StorageTarget.USER)
|
||||
// @ISecretStorageService private readonly _secretStorageService: ISecretStorageService,
|
||||
) {
|
||||
@@ -148,7 +151,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
||||
}
|
||||
}
|
||||
|
||||
const newFeatureFlags = this.state.featureFlagSettings
|
||||
const newGlobalSettings = this.state.globalSettings
|
||||
|
||||
// if changed models or enabled a provider, recompute models list
|
||||
const modelsListChanged = settingName === 'models' || settingName === '_enabled'
|
||||
@@ -157,7 +160,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
||||
const newState: VoidSettingsState = {
|
||||
modelSelectionOfFeature: newModelSelectionOfFeature,
|
||||
settingsOfProvider: newSettingsOfProvider,
|
||||
featureFlagSettings: newFeatureFlags,
|
||||
globalSettings: newGlobalSettings,
|
||||
_modelOptions: newModelsList,
|
||||
}
|
||||
|
||||
@@ -169,11 +172,11 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
||||
for (const featureName of featureNames) {
|
||||
|
||||
const currentSelection = newModelSelectionOfFeature[featureName]
|
||||
const selnIdx = currentSelection === null ? -1 : newModelsList.findIndex(m => modelSelectionsEqual(m.value, currentSelection))
|
||||
const selnIdx = currentSelection === null ? -1 : newModelsList.findIndex(m => modelSelectionsEqual(m.selection, currentSelection))
|
||||
|
||||
if (selnIdx === -1) {
|
||||
if (newModelsList.length !== 0)
|
||||
this.setModelSelectionOfFeature(featureName, newModelsList[0].value, { doNotApplyEffects: true })
|
||||
this.setModelSelectionOfFeature(featureName, newModelsList[0].selection, { doNotApplyEffects: true })
|
||||
else
|
||||
this.setModelSelectionOfFeature(featureName, null, { doNotApplyEffects: true })
|
||||
}
|
||||
@@ -185,17 +188,17 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
||||
}
|
||||
|
||||
|
||||
setFeatureFlag: SetFeatureFlagFn = async (flagName, newVal) => {
|
||||
const newState = {
|
||||
setGlobalSetting: SetGlobalSettingFn = async (settingName, newVal) => {
|
||||
const newState: VoidSettingsState = {
|
||||
...this.state,
|
||||
featureFlagSettings: {
|
||||
...this.state.featureFlagSettings,
|
||||
[flagName]: newVal
|
||||
globalSettings: {
|
||||
...this.state.globalSettings,
|
||||
[settingName]: newVal
|
||||
}
|
||||
}
|
||||
this.state = newState
|
||||
await this._storeState()
|
||||
this._onDidChangeState.fire('featureFlagSettings')
|
||||
this._onDidChangeState.fire(['globalSettings', settingName])
|
||||
|
||||
}
|
||||
|
||||
@@ -220,25 +223,45 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
||||
|
||||
|
||||
|
||||
setDefaultModels(providerName: ProviderName, newDefaultModelNames: string[]) {
|
||||
setAutodetectedModels(providerName: ProviderName, newDefaultModelNames: string[], logging: object) {
|
||||
|
||||
const { models } = this.state.settingsOfProvider[providerName]
|
||||
|
||||
const old_names = models.map(m => m.modelName)
|
||||
|
||||
const newDefaultModels = modelInfoOfDefaultNames(newDefaultModelNames, { isAutodetected: true, existingModels: models })
|
||||
const newModels = [
|
||||
...newDefaultModels,
|
||||
...models.filter(m => !m.isDefault), // keep any non-default models
|
||||
]
|
||||
|
||||
|
||||
this.setSettingOfProvider(providerName, 'models', newModels)
|
||||
|
||||
// if the models changed, log it
|
||||
const new_names = newModels.map(m => m.modelName)
|
||||
if (!(old_names.length === new_names.length
|
||||
&& old_names.every((_, i) => old_names[i] === new_names[i])
|
||||
)) {
|
||||
this._metricsService.capture('Autodetect Models', { providerName, newModels, ...logging })
|
||||
}
|
||||
}
|
||||
toggleModelHidden(providerName: ProviderName, modelName: string) {
|
||||
|
||||
|
||||
const { models } = this.state.settingsOfProvider[providerName]
|
||||
const modelIdx = models.findIndex(m => m.modelName === modelName)
|
||||
if (modelIdx === -1) return
|
||||
const newIsHidden = !models[modelIdx].isHidden
|
||||
const newModels: VoidModelInfo[] = [
|
||||
...models.slice(0, modelIdx),
|
||||
{ ...models[modelIdx], isHidden: !models[modelIdx].isHidden },
|
||||
{ ...models[modelIdx], isHidden: newIsHidden },
|
||||
...models.slice(modelIdx + 1, Infinity)
|
||||
]
|
||||
this.setSettingOfProvider(providerName, 'models', newModels)
|
||||
|
||||
this._metricsService.capture('Toggle Model Hidden', { providerName, modelName, newIsHidden })
|
||||
|
||||
}
|
||||
addModel(providerName: ProviderName, modelName: string) {
|
||||
const { models } = this.state.settingsOfProvider[providerName]
|
||||
@@ -249,6 +272,9 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
||||
{ modelName, isDefault: false, isHidden: false }
|
||||
]
|
||||
this.setSettingOfProvider(providerName, 'models', newModels)
|
||||
|
||||
this._metricsService.capture('Add Model', { providerName, modelName })
|
||||
|
||||
}
|
||||
deleteModel(providerName: ProviderName, modelName: string): boolean {
|
||||
const { models } = this.state.settingsOfProvider[providerName]
|
||||
@@ -259,6 +285,9 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
||||
...models.slice(delIdx + 1, Infinity)
|
||||
]
|
||||
this.setSettingOfProvider(providerName, 'models', newModels)
|
||||
|
||||
this._metricsService.capture('Delete Model', { providerName, modelName })
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
|
||||
/*------------------------------------------------------------------------------------------
|
||||
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
|
||||
*-----------------------------------------------------------------------------------------*/
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
|
||||
|
||||
@@ -14,26 +14,33 @@ export type VoidModelInfo = {
|
||||
isAutodetected?: boolean, // whether the model was autodetected by polling
|
||||
}
|
||||
|
||||
type ModelInfoOfDefaultNamesOptions = { isAutodetected: true, existingModels: VoidModelInfo[] } // | { isOtherOption: true, ...otherOptions }
|
||||
export const modelInfoOfDefaultNames = (modelNames: string[], options?: ModelInfoOfDefaultNamesOptions): VoidModelInfo[] => {
|
||||
// creates `modelInfo` from `modelNames`
|
||||
export const modelInfoOfDefaultNames = (modelNames: string[], options?: { isAutodetected: true, existingModels: VoidModelInfo[] }): VoidModelInfo[] => {
|
||||
|
||||
const { isAutodetected, existingModels } = options ?? {}
|
||||
const isDefault = true
|
||||
const isHidden = modelNames.length >= 10 // hide all models if there are a ton of them, and make user enable them individually
|
||||
|
||||
if (!existingModels) {
|
||||
if (!existingModels) { // default settings
|
||||
|
||||
return modelNames.map((modelName, i) => ({ modelName, isDefault, isAutodetected, isHidden, }))
|
||||
return modelNames.map((modelName, i) => ({
|
||||
modelName,
|
||||
isDefault: true,
|
||||
isAutodetected: isAutodetected,
|
||||
isHidden: modelNames.length >= 10 // hide all models if there are a ton of them, and make user enable them individually
|
||||
}))
|
||||
|
||||
} else {
|
||||
// keep existing `isHidden` property
|
||||
} else { // settings if there are existing models (keep existing `isHidden` property)
|
||||
|
||||
const existingModelsMap: Record<string, VoidModelInfo> = {}
|
||||
for (const em of existingModels) {
|
||||
existingModelsMap[em.modelName] = em
|
||||
for (const existingModel of existingModels) {
|
||||
existingModelsMap[existingModel.modelName] = existingModel
|
||||
}
|
||||
|
||||
return modelNames.map((modelName, i) => ({ modelName, isDefault, isAutodetected, isHidden: !!existingModelsMap[modelName]?.isHidden, }))
|
||||
return modelNames.map((modelName, i) => ({
|
||||
modelName,
|
||||
isDefault: true,
|
||||
isAutodetected: isAutodetected,
|
||||
isHidden: !!existingModelsMap[modelName]?.isHidden,
|
||||
}))
|
||||
|
||||
}
|
||||
|
||||
@@ -144,7 +151,7 @@ export const defaultProviderSettings = {
|
||||
export type ProviderName = keyof typeof defaultProviderSettings
|
||||
export const providerNames = Object.keys(defaultProviderSettings) as ProviderName[]
|
||||
|
||||
export const localProviderNames = ['ollama', 'openAICompatible'] satisfies ProviderName[] // all local names
|
||||
export const localProviderNames = ['ollama'] satisfies ProviderName[] // all local names
|
||||
export const nonlocalProviderNames = providerNames.filter((name) => !(localProviderNames as string[]).includes(name)) // all non-local names
|
||||
|
||||
type CustomSettingName = UnionOfKeys<typeof defaultProviderSettings[ProviderName]>
|
||||
@@ -177,6 +184,7 @@ export type SettingName = keyof SettingsForProvider<ProviderName>
|
||||
|
||||
type DisplayInfoForProviderName = {
|
||||
title: string,
|
||||
desc?: string,
|
||||
}
|
||||
|
||||
export const displayInfoOfProviderName = (providerName: ProviderName): DisplayInfoForProviderName => {
|
||||
@@ -203,7 +211,7 @@ export const displayInfoOfProviderName = (providerName: ProviderName): DisplayIn
|
||||
}
|
||||
else if (providerName === 'openAICompatible') {
|
||||
return {
|
||||
title: 'Other',
|
||||
title: 'OpenAI-Compatible',
|
||||
}
|
||||
}
|
||||
else if (providerName === 'gemini') {
|
||||
@@ -256,7 +264,7 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
|
||||
: providerName === 'openAICompatible' ? 'https://my-website.com/v1'
|
||||
: '(never)',
|
||||
|
||||
subTextMd: providerName === 'ollama' ? 'Read about advanced [Endpoints here](https://github.com/ollama/ollama/blob/main/docs/faq.md#how-can-i-expose-ollama-on-my-network).' :
|
||||
subTextMd: providerName === 'ollama' ? 'If you would like to change this endpoint, please read more about [Endpoints here](https://github.com/ollama/ollama/blob/main/docs/faq.md#how-can-i-expose-ollama-on-my-network).' :
|
||||
undefined,
|
||||
}
|
||||
}
|
||||
@@ -389,26 +397,16 @@ export type RefreshableProviderName = typeof refreshableProviderNames[number]
|
||||
|
||||
|
||||
|
||||
export type FeatureFlagSettings = {
|
||||
export type GlobalSettings = {
|
||||
autoRefreshModels: boolean;
|
||||
aiInstructions: string;
|
||||
}
|
||||
export const defaultFeatureFlagSettings: FeatureFlagSettings = {
|
||||
export const defaultGlobalSettings: GlobalSettings = {
|
||||
autoRefreshModels: true,
|
||||
aiInstructions: '',
|
||||
}
|
||||
|
||||
export type FeatureFlagName = keyof FeatureFlagSettings
|
||||
export const featureFlagNames = Object.keys(defaultFeatureFlagSettings) as FeatureFlagName[]
|
||||
|
||||
type FeatureFlagDisplayInfo = {
|
||||
description: string,
|
||||
}
|
||||
export const displayInfoOfFeatureFlag = (featureFlag: FeatureFlagName): FeatureFlagDisplayInfo => {
|
||||
if (featureFlag === 'autoRefreshModels') {
|
||||
return {
|
||||
description: `Automatically detect local providers and models (${refreshableProviderNames.map(providerName => displayInfoOfProviderName(providerName).title).join(', ')}).`,
|
||||
}
|
||||
}
|
||||
throw new Error(`featureFlagInfo: Unknown feature flag: "${featureFlag}"`)
|
||||
}
|
||||
export type GlobalSettingName = keyof GlobalSettings
|
||||
export const globalSettingNames = Object.keys(defaultGlobalSettings) as GlobalSettingName[]
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { createDecorator } from '../../instantiation/common/instantiation.js';
|
||||
import { ProxyChannel } from '../../../base/parts/ipc/common/ipc.js';
|
||||
import { IMainProcessService } from '../../ipc/common/mainProcessService.js';
|
||||
import { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js';
|
||||
|
||||
|
||||
|
||||
export interface IVoidUpdateService {
|
||||
readonly _serviceBrand: undefined;
|
||||
check: () => Promise<{ hasUpdate: true, message: string } | { hasUpdate: false } | null>;
|
||||
}
|
||||
|
||||
|
||||
export const IVoidUpdateService = createDecorator<IVoidUpdateService>('VoidUpdateService');
|
||||
|
||||
|
||||
// implemented by calling channel
|
||||
export class VoidUpdateService implements IVoidUpdateService {
|
||||
|
||||
readonly _serviceBrand: undefined;
|
||||
private readonly voidUpdateService: IVoidUpdateService;
|
||||
|
||||
constructor(
|
||||
@IMainProcessService mainProcessService: IMainProcessService, // (only usable on client side)
|
||||
) {
|
||||
// creates an IPC proxy to use metricsMainService.ts
|
||||
this.voidUpdateService = ProxyChannel.toService<IVoidUpdateService>(mainProcessService.getChannel('void-channel-update'));
|
||||
}
|
||||
|
||||
|
||||
|
||||
// anything transmitted over a channel must be async even if it looks like it doesn't have to be
|
||||
check: IVoidUpdateService['check'] = async () => {
|
||||
const res = await this.voidUpdateService.check()
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
registerSingleton(IVoidUpdateService, VoidUpdateService, InstantiationType.Eager);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*------------------------------------------------------------------------------------------
|
||||
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
|
||||
*-----------------------------------------------------------------------------------------*/
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import Anthropic from '@anthropic-ai/sdk';
|
||||
import { _InternalSendLLMMessageFnType } from '../../common/llmMessageTypes.js';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*------------------------------------------------------------------------------------------
|
||||
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
|
||||
*-----------------------------------------------------------------------------------------*/
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Content, GoogleGenerativeAI, GoogleGenerativeAIFetchError } from '@google/generative-ai';
|
||||
import { _InternalSendLLMMessageFnType } from '../../common/llmMessageTypes.js';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*------------------------------------------------------------------------------------------
|
||||
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
|
||||
*-----------------------------------------------------------------------------------------*/
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
// // Greptile
|
||||
// // https://docs.greptile.com/api-reference/query
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*------------------------------------------------------------------------------------------
|
||||
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
|
||||
*-----------------------------------------------------------------------------------------*/
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import Groq from 'groq-sdk';
|
||||
import { _InternalSendLLMMessageFnType } from '../../common/llmMessageTypes.js';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*------------------------------------------------------------------------------------------
|
||||
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
|
||||
*-----------------------------------------------------------------------------------------*/
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Ollama } from 'ollama';
|
||||
import { _InternalModelListFnType, _InternalSendLLMMessageFnType, OllamaModelResponse } from '../../common/llmMessageTypes.js';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*------------------------------------------------------------------------------------------
|
||||
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
|
||||
*-----------------------------------------------------------------------------------------*/
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import OpenAI from 'openai';
|
||||
import { _InternalModelListFnType, _InternalSendLLMMessageFnType } from '../../common/llmMessageTypes.js';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*------------------------------------------------------------------------------------------
|
||||
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
|
||||
*-----------------------------------------------------------------------------------------*/
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { LLMMMessageParams, OnText, OnFinalMessage, OnError } from '../../common/llmMessageTypes.js';
|
||||
import { IMetricsService } from '../../common/metricsService.js';
|
||||
@@ -34,9 +34,9 @@ export const sendLLMMessage = ({
|
||||
const captureChatEvent = (eventId: string, extras?: object) => {
|
||||
metricsService.capture(eventId, {
|
||||
providerName,
|
||||
modelName,
|
||||
numMessages: messages?.length,
|
||||
messagesShape: messages?.map(msg => ({ role: msg.role, length: msg.content.length })),
|
||||
version: '2024-11-14',
|
||||
...extras,
|
||||
})
|
||||
}
|
||||
@@ -61,7 +61,6 @@ export const sendLLMMessage = ({
|
||||
|
||||
const onError: OnError = ({ message: error, fullError }) => {
|
||||
if (_didAbort) return
|
||||
console.log("ERROR!!!!!", error)
|
||||
console.error('sendLLMMessage onError:', error)
|
||||
captureChatEvent(`${loggingName} - Error`, { error })
|
||||
onError_({ message: error, fullError })
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*------------------------------------------------------------------------------------------
|
||||
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
|
||||
*-----------------------------------------------------------------------------------------*/
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
// registered in app.ts
|
||||
// code convention is to make a service responsible for this stuff, and not a channel, but having fewer files is simpler...
|
||||
|
||||
@@ -1,46 +1,100 @@
|
||||
/*------------------------------------------------------------------------------------------
|
||||
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
|
||||
*-----------------------------------------------------------------------------------------*/
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* 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 { ITelemetryService } from '../../telemetry/common/telemetry.js';
|
||||
import { isLinux, isMacintosh, isWindows } from '../../../base/common/platform.js';
|
||||
import { generateUuid } from '../../../base/common/uuid.js';
|
||||
import { IEnvironmentMainService } from '../../environment/electron-main/environmentMainService.js';
|
||||
|
||||
import { IProductService } from '../../product/common/productService.js';
|
||||
import { IStorageMainService } from '../../storage/electron-main/storageMainService.js';
|
||||
|
||||
import { IMetricsService } from '../common/metricsService.js';
|
||||
import { PostHog } from 'posthog-node'
|
||||
|
||||
|
||||
// posthog-js (old):
|
||||
// posthog.init('phc_UanIdujHiLp55BkUTjB1AuBXcasVkdqRwgnwRlWESH2', { api_host: 'https://us.i.posthog.com', })
|
||||
const os = isWindows ? 'windows' : isMacintosh ? 'mac' : isLinux ? 'linux' : null
|
||||
|
||||
// const buildEnv = 'development';
|
||||
// const buildNumber = '1.0.0';
|
||||
// const isMac = process.platform === 'darwin';
|
||||
const VOID_MACHINE_STORAGE_KEY = 'void.machineId'
|
||||
|
||||
export class MetricsMainService extends Disposable implements IMetricsService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
readonly _distinctId: string
|
||||
readonly client: PostHog
|
||||
private readonly client: PostHog
|
||||
|
||||
private readonly _initProperties: object
|
||||
|
||||
|
||||
// TODO we should eventually identify people based on email
|
||||
private get machineId() {
|
||||
const currVal = this._storageService.applicationStorage.get(VOID_MACHINE_STORAGE_KEY)
|
||||
if (currVal !== undefined) return currVal
|
||||
const newVal = generateUuid()
|
||||
this._storageService.applicationStorage.set(VOID_MACHINE_STORAGE_KEY, newVal)
|
||||
return newVal
|
||||
}
|
||||
|
||||
|
||||
constructor(
|
||||
@ITelemetryService private readonly _telemetryService: ITelemetryService
|
||||
@IProductService private readonly _productService: IProductService,
|
||||
@IStorageMainService private readonly _storageService: IStorageMainService,
|
||||
@IEnvironmentMainService private readonly _envMainService: IEnvironmentMainService,
|
||||
) {
|
||||
super()
|
||||
this.client = new PostHog('phc_UanIdujHiLp55BkUTjB1AuBXcasVkdqRwgnwRlWESH2', { host: 'https://us.i.posthog.com', })
|
||||
this.client = new PostHog('phc_UanIdujHiLp55BkUTjB1AuBXcasVkdqRwgnwRlWESH2', {
|
||||
host: 'https://us.i.posthog.com',
|
||||
})
|
||||
|
||||
const { devDeviceId, firstSessionDate, machineId } = this._telemetryService
|
||||
this._distinctId = devDeviceId
|
||||
this.client.identify({ distinctId: devDeviceId, properties: { firstSessionDate, machineId } })
|
||||
// we'd like to use devDeviceId on telemetryService, but that gets sanitized by the time it gets here as 'someValue.devDeviceId'
|
||||
|
||||
console.log('Void posthog metrics info:', JSON.stringify({ devDeviceId, firstSessionDate, machineId }))
|
||||
const { commit, version, quality } = this._productService
|
||||
|
||||
const isDevMode = !this._envMainService.isBuilt // found in abstractUpdateService.ts
|
||||
|
||||
|
||||
// custom properties we identify
|
||||
this._initProperties = {
|
||||
commit,
|
||||
version,
|
||||
os,
|
||||
quality,
|
||||
distinctId: this.machineId,
|
||||
isDevMode,
|
||||
...this._getOSInfo(),
|
||||
}
|
||||
|
||||
const identifyMessage = {
|
||||
distinctId: this.machineId,
|
||||
properties: this._initProperties,
|
||||
}
|
||||
this.client.identify(identifyMessage)
|
||||
|
||||
console.log('Void posthog metrics info:', JSON.stringify(identifyMessage, null, 2))
|
||||
|
||||
}
|
||||
|
||||
_getOSInfo() {
|
||||
try {
|
||||
const { platform, arch } = process // see platform.ts
|
||||
return { platform, arch }
|
||||
}
|
||||
catch (e) {
|
||||
return { osInfo: { platform: '??', arch: '??' } }
|
||||
}
|
||||
}
|
||||
|
||||
capture: IMetricsService['capture'] = (event, params) => {
|
||||
const capture = { distinctId: this._distinctId, event, properties: params } as const
|
||||
const capture = { distinctId: this.machineId, event, properties: params } as const
|
||||
// console.log('full capture:', capture)
|
||||
this.client.capture(capture)
|
||||
}
|
||||
|
||||
|
||||
async getDebuggingProperties() {
|
||||
return this._initProperties
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* 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 { IEnvironmentMainService } from '../../environment/electron-main/environmentMainService.js';
|
||||
|
||||
import { IProductService } from '../../product/common/productService.js';
|
||||
|
||||
import { IVoidUpdateService } from '../common/voidUpdateService.js';
|
||||
|
||||
|
||||
|
||||
export class VoidMainUpdateService extends Disposable implements IVoidUpdateService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
constructor(
|
||||
@IProductService private readonly _productService: IProductService,
|
||||
@IEnvironmentMainService private readonly _envMainService: IEnvironmentMainService,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
async check() {
|
||||
const isDevMode = !this._envMainService.isBuilt // found in abstractUpdateService.ts
|
||||
|
||||
if (isDevMode) {
|
||||
return { hasUpdate: false } as const
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`https://updates.voideditor.dev/api/v0/${this._productService.commit}`)
|
||||
const resJSON = await res.json()
|
||||
|
||||
if (!resJSON) return null
|
||||
|
||||
const { hasUpdate, downloadMessage } = resJSON ?? {}
|
||||
if (hasUpdate === undefined)
|
||||
return null
|
||||
|
||||
const after = (downloadMessage || '') + ''
|
||||
return { hasUpdate: !!hasUpdate, message: after }
|
||||
}
|
||||
catch (e) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2534,7 +2534,7 @@ const LayoutStateKeys = {
|
||||
// Part Sizing
|
||||
GRID_SIZE: new InitializationStateKey('grid.size', StorageScope.PROFILE, StorageTarget.MACHINE, { width: 800, height: 600 }),
|
||||
SIDEBAR_SIZE: new InitializationStateKey<number>('sideBar.size', StorageScope.PROFILE, StorageTarget.MACHINE, 200),
|
||||
AUXILIARYBAR_SIZE: new InitializationStateKey<number>('auxiliaryBar.size', StorageScope.PROFILE, StorageTarget.MACHINE, 200),
|
||||
AUXILIARYBAR_SIZE: new InitializationStateKey<number>('auxiliaryBar.size', StorageScope.PROFILE, StorageTarget.MACHINE, 800), // Void changed this from 200 to 800
|
||||
PANEL_SIZE: new InitializationStateKey<number>('panel.size', StorageScope.PROFILE, StorageTarget.MACHINE, 300),
|
||||
|
||||
PANEL_LAST_NON_MAXIMIZED_HEIGHT: new RuntimeStateKey<number>('panel.lastNonMaximizedHeight', StorageScope.PROFILE, StorageTarget.MACHINE, 300),
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><style>.st0{fill:#f6f6f6;fill-opacity:0}.st1{fill:#fff}.st2{fill:#167abf}</style><path class="st0" d="M1024 1024H0V0h1024v1024z"/><path class="st1" d="M1024 85.333v853.333H0V85.333h1024z"/><path class="st2" d="M0 85.333h298.667v853.333H0V85.333zm1024 0v853.333H384V85.333h640zm-554.667 160h341.333v-64H469.333v64zm341.334 533.334H469.333v64h341.333l.001-64zm128-149.334H597.333v64h341.333l.001-64zm0-149.333H597.333v64h341.333l.001-64zm0-149.333H597.333v64h341.333l.001-64z"/></svg>
|
||||
|
Before Width: | Height: | Size: 559 B |
Binary file not shown.
|
After Width: | Height: | Size: 795 KiB |
@@ -45,7 +45,7 @@ export class AuxiliaryBarPart extends AbstractPaneCompositePart {
|
||||
static readonly viewContainersWorkspaceStateKey = 'workbench.auxiliarybar.viewContainersWorkspaceState';
|
||||
|
||||
// Use the side bar dimensions
|
||||
override readonly minimumWidth: number = 170;
|
||||
override readonly minimumWidth: number = 230; // Void changed this (was 170)
|
||||
override readonly maximumWidth: number = Number.POSITIVE_INFINITY;
|
||||
override readonly minimumHeight: number = 0;
|
||||
override readonly maximumHeight: number = Number.POSITIVE_INFINITY;
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
background-size: 16px;
|
||||
background-image: url('../../../../browser/media/code-icon.svg');
|
||||
background-image: url('../../../../browser/media/void-icon-sm.png');
|
||||
width: 16px;
|
||||
padding: 0;
|
||||
margin: 0 6px 0 10px;
|
||||
|
||||
@@ -18,14 +18,13 @@ import { isRecentFolder, IWorkspacesService } from '../../../../platform/workspa
|
||||
import { ICommandService } from '../../../../platform/commands/common/commands.js';
|
||||
import { OpenFileFolderAction, OpenFolderAction } from '../../actions/workspaceActions.js';
|
||||
import { isMacintosh, isNative, OS } from '../../../../base/common/platform.js';
|
||||
import { VOID_CTRL_L_ACTION_ID } from '../../../contrib/void/browser/sidebarActions.js';
|
||||
import { VOID_CTRL_K_ACTION_ID } from '../../../contrib/void/browser/quickEditActions.js';
|
||||
import { defaultKeybindingLabelStyles } from '../../../../platform/theme/browser/defaultStyles.js';
|
||||
import { IWindowOpenable } from '../../../../platform/window/common/window.js';
|
||||
import { ILabelService, Verbosity } from '../../../../platform/label/common/label.js';
|
||||
import { splitRecentLabel } from '../../../../base/common/labels.js';
|
||||
import { IHostService } from '../../../services/host/browser/host.js';
|
||||
import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../contrib/void/browser/voidSettingsPane.js';
|
||||
import { VOID_CTRL_K_ACTION_ID, VOID_CTRL_L_ACTION_ID } from '../../../contrib/void/browser/actionIDs.js';
|
||||
// import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
|
||||
|
||||
registerColor('editorWatermark.foreground', { dark: transparent(editorForeground, 0.6), light: transparent(editorForeground, 0.68), hcDark: editorForeground, hcLight: editorForeground }, localize('editorLineHighlight', 'Foreground color for the labels in the editor watermark.'));
|
||||
@@ -168,17 +167,17 @@ export class EditorGroupWatermark extends Disposable {
|
||||
// .filter(entry => !!this.keybindingService.lookupKeybinding(entry.id));
|
||||
|
||||
this.clear();
|
||||
const box = append(this.shortcuts, $('.watermark-box'));
|
||||
const boxBelow = append(this.shortcuts, $(''))
|
||||
boxBelow.style.display = 'flex'
|
||||
boxBelow.style.flex = 'row'
|
||||
boxBelow.style.justifyContent = 'center'
|
||||
const voidIconBox = append(this.shortcuts, $('.watermark-box'));
|
||||
const recentsBox = append(this.shortcuts, $('div'));
|
||||
recentsBox.style.display = 'flex'
|
||||
recentsBox.style.flex = 'row'
|
||||
recentsBox.style.justifyContent = 'center'
|
||||
|
||||
|
||||
const update = async () => {
|
||||
|
||||
clearNode(box);
|
||||
clearNode(boxBelow);
|
||||
clearNode(voidIconBox);
|
||||
clearNode(recentsBox);
|
||||
|
||||
this.currentDisposables.forEach(label => label.dispose());
|
||||
this.currentDisposables.clear();
|
||||
@@ -188,13 +187,14 @@ export class EditorGroupWatermark extends Disposable {
|
||||
if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) {
|
||||
|
||||
// Open a folder
|
||||
const button = h('button')
|
||||
button.root.classList.add('void-watermark-button')
|
||||
button.root.style.display = 'block'
|
||||
button.root.style.marginLeft = 'auto'
|
||||
button.root.style.marginRight = 'auto'
|
||||
button.root.textContent = 'Open a folder'
|
||||
button.root.onclick = () => {
|
||||
const openFolderButton = h('button')
|
||||
openFolderButton.root.classList.add('void-watermark-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.onclick = () => {
|
||||
this.commandService.executeCommand(isMacintosh && isNative ? OpenFileFolderAction.ID : OpenFolderAction.ID)
|
||||
// if (this.contextKeyService.contextMatchesRules(ContextKeyExpr.and(WorkbenchStateContext.isEqualTo('workspace')))) {
|
||||
// this.commandService.executeCommand(OpenFolderViaWorkspaceAction.ID);
|
||||
@@ -202,7 +202,7 @@ export class EditorGroupWatermark extends Disposable {
|
||||
// this.commandService.executeCommand(isMacintosh ? 'workbench.action.files.openFileFolder' : 'workbench.action.files.openFolder');
|
||||
// }
|
||||
}
|
||||
box.appendChild(button.root);
|
||||
voidIconBox.appendChild(openFolderButton.root);
|
||||
|
||||
|
||||
// Recents
|
||||
@@ -212,13 +212,8 @@ export class EditorGroupWatermark extends Disposable {
|
||||
|
||||
if (recentlyOpened.length !== 0) {
|
||||
|
||||
const span = $('div')
|
||||
span.textContent = 'Recent'
|
||||
span.style.fontWeight = '500'
|
||||
box.append(span)
|
||||
|
||||
box.append(
|
||||
...recentlyOpened.map(w => {
|
||||
voidIconBox.append(
|
||||
...recentlyOpened.map((w, i) => {
|
||||
|
||||
let fullPath: string;
|
||||
let windowOpenable: IWindowOpenable;
|
||||
@@ -235,14 +230,13 @@ export class EditorGroupWatermark extends Disposable {
|
||||
|
||||
const { name, parentPath } = splitRecentLabel(fullPath);
|
||||
|
||||
const li = $('li');
|
||||
const link = $('span');
|
||||
link.classList.add('void-link')
|
||||
const linkSpan = $('span');
|
||||
linkSpan.classList.add('void-link')
|
||||
linkSpan.style.display = 'flex'
|
||||
linkSpan.style.gap = '4px'
|
||||
linkSpan.style.padding = '8px'
|
||||
|
||||
link.innerText = name;
|
||||
link.title = fullPath;
|
||||
link.setAttribute('aria-label', localize('welcomePage.openFolderWithPath', "Open folder {0} with path {1}", name, parentPath));
|
||||
link.addEventListener('click', e => {
|
||||
linkSpan.addEventListener('click', e => {
|
||||
this.hostService.openWindow([windowOpenable], {
|
||||
forceNewWindow: e.ctrlKey || e.metaKey,
|
||||
remoteAuthority: w.remoteAuthority || null // local window if remoteAuthority is not set or can not be deducted from the openable
|
||||
@@ -250,29 +244,30 @@ export class EditorGroupWatermark extends Disposable {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
});
|
||||
li.appendChild(link);
|
||||
|
||||
const span = $('span');
|
||||
span.style.paddingLeft = '4px';
|
||||
span.classList.add('path');
|
||||
span.classList.add('detail');
|
||||
span.innerText = parentPath;
|
||||
span.title = fullPath;
|
||||
li.appendChild(span);
|
||||
const nameSpan = $('span');
|
||||
nameSpan.innerText = name;
|
||||
nameSpan.title = fullPath;
|
||||
linkSpan.appendChild(nameSpan);
|
||||
|
||||
return li
|
||||
const dirSpan = $('span');
|
||||
dirSpan.style.paddingLeft = '4px';
|
||||
dirSpan.innerText = parentPath;
|
||||
dirSpan.title = fullPath;
|
||||
|
||||
linkSpan.appendChild(dirSpan);
|
||||
|
||||
return linkSpan
|
||||
}).filter(v => !!v)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
else {
|
||||
|
||||
// show them Void keybindings
|
||||
const keys = this.keybindingService.lookupKeybinding(VOID_CTRL_L_ACTION_ID);
|
||||
const dl = append(box, $('dl'));
|
||||
const dl = append(voidIconBox, $('dl'));
|
||||
const dt = append(dl, $('dt'));
|
||||
dt.textContent = 'Chat'
|
||||
const dd = append(dl, $('dd'));
|
||||
@@ -283,7 +278,7 @@ export class EditorGroupWatermark extends Disposable {
|
||||
|
||||
|
||||
const keys2 = this.keybindingService.lookupKeybinding(VOID_CTRL_K_ACTION_ID);
|
||||
const dl2 = append(box, $('dl'));
|
||||
const dl2 = append(voidIconBox, $('dl'));
|
||||
const dt2 = append(dl2, $('dt'));
|
||||
dt2.textContent = 'Quick Edit'
|
||||
const dd2 = append(dl2, $('dd'));
|
||||
@@ -293,7 +288,7 @@ export class EditorGroupWatermark extends Disposable {
|
||||
this.currentDisposables.add(label2);
|
||||
|
||||
const keys3 = this.keybindingService.lookupKeybinding('workbench.action.openGlobalKeybindings');
|
||||
const button3 = append(boxBelow, $('button'));
|
||||
const button3 = append(recentsBox, $('button'));
|
||||
button3.textContent = 'Void Settings'
|
||||
button3.style.display = 'block'
|
||||
button3.style.marginLeft = 'auto'
|
||||
|
||||
@@ -253,7 +253,7 @@
|
||||
}
|
||||
|
||||
.monaco-workbench .part.titlebar > .titlebar-container > .titlebar-left > .window-appicon:not(.codicon) {
|
||||
background-image: url('../../../media/code-icon.svg');
|
||||
background-image: url('../../../media/void-icon-sm.png');
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
background-size: 16px;
|
||||
@@ -275,7 +275,7 @@
|
||||
height: 8px;
|
||||
z-index: 1;
|
||||
/* on top of home indicator */
|
||||
background-image: url('../../../media/code-icon.svg');
|
||||
background-image: url('../../../media/void-icon-sm.png');
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
background-size: 8px;
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
|
||||
.file-icons-enabled .show-file-icons .webview-vs_code_release_notes-name-file-icon.file-icon::before {
|
||||
content: ' ';
|
||||
background-image: url('../../../../browser/media/code-icon.svg');
|
||||
background-image: url('../../../../browser/media/void-icon-sm.png');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
// Normally you'd want to put these exports in the files that register them, but if you do that you'll get an import order error if you import them in certain cases.
|
||||
// (importing them runs the whole file to get the ID, causing an import error). I guess it's best practice to separate out IDs, pretty annoying...
|
||||
|
||||
export const VOID_CTRL_L_ACTION_ID = 'void.ctrlLAction'
|
||||
|
||||
export const VOID_CTRL_K_ACTION_ID = 'void.ctrlKAction'
|
||||
@@ -1,7 +1,7 @@
|
||||
/*------------------------------------------------------------------------------------------
|
||||
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
|
||||
*-----------------------------------------------------------------------------------------*/
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* 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 { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js';
|
||||
@@ -17,7 +17,7 @@ import { IEditorService } from '../../../services/editor/common/editorService.js
|
||||
import { isCodeEditor } from '../../../../editor/browser/editorBrowser.js';
|
||||
import { EditorResourceAccessor } from '../../../common/editor.js';
|
||||
import { IModelService } from '../../../../editor/common/services/model.js';
|
||||
import { extractCodeFromResult } from './helpers/extractCodeFromResult.js';
|
||||
import { extractCodeFromRegular } from './helpers/extractCodeFromResult.js';
|
||||
|
||||
// The extension this was called from is here - https://github.com/voideditor/void/blob/autocomplete/extensions/void/src/extension/extension.ts
|
||||
|
||||
@@ -652,7 +652,8 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
|
||||
// newAutocompletion.abortRef = { current: () => { } }
|
||||
newAutocompletion.status = 'finished'
|
||||
// newAutocompletion.promise = undefined
|
||||
newAutocompletion.insertText = postprocessResult(extractCodeFromResult(fullText))
|
||||
const [text, _] = extractCodeFromRegular({ text: fullText, recentlyAddedTextLen: 0 })
|
||||
newAutocompletion.insertText = postprocessResult(text)
|
||||
|
||||
resolve(newAutocompletion.insertText)
|
||||
|
||||
@@ -662,7 +663,7 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
|
||||
newAutocompletion.status = 'error'
|
||||
reject(message)
|
||||
},
|
||||
featureName: 'Autocomplete',
|
||||
useProviderFor: 'Autocomplete',
|
||||
range: { startLineNumber: position.lineNumber, startColumn: position.column, endLineNumber: position.lineNumber, endColumn: position.column },
|
||||
})
|
||||
newAutocompletion.requestId = requestId
|
||||
|
||||
@@ -0,0 +1,302 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* 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 { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
|
||||
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
import { Emitter, Event } from '../../../../base/common/event.js';
|
||||
import { IRange } from '../../../../editor/common/core/range.js';
|
||||
import { ILLMMessageService } from '../../../../platform/void/common/llmMessageService.js';
|
||||
import { IModelService } from '../../../../editor/common/services/model.js';
|
||||
import { VSReadFile } from './helpers/readFile.js';
|
||||
import { chat_prompt, chat_systemMessage } from './prompt/prompts.js';
|
||||
|
||||
export type CodeSelection = {
|
||||
fileURI: URI;
|
||||
selectionStr: string | null;
|
||||
content: string; // TODO remove this (replace `selectionStr` with `content`)
|
||||
range: IRange;
|
||||
}
|
||||
|
||||
// if selectionStr is null, it means to use the entire file at send time
|
||||
export type CodeStagingSelection = {
|
||||
type: 'Selection',
|
||||
fileURI: URI,
|
||||
selectionStr: string,
|
||||
range: IRange
|
||||
} | {
|
||||
type: 'File',
|
||||
fileURI: URI,
|
||||
selectionStr: null,
|
||||
range: null
|
||||
}
|
||||
|
||||
|
||||
// WARNING: changing this format is a big deal!!!!!! need to migrate old format to new format on users' computers so people don't get errors.
|
||||
export type ChatMessage =
|
||||
| {
|
||||
role: 'user';
|
||||
content: string | null; // content sent to the llm - allowed to be '', will be replaced with (empty)
|
||||
displayContent: string | null; // content displayed to user - allowed to be '', will be ignored
|
||||
selections: CodeSelection[] | null; // the user's selection
|
||||
}
|
||||
| {
|
||||
role: 'assistant';
|
||||
content: string | null; // content received from LLM - allowed to be '', will be replaced with (empty)
|
||||
displayContent: string | null; // content displayed to user (this is the same as content for now) - allowed to be '', will be ignored
|
||||
}
|
||||
| {
|
||||
role: 'system';
|
||||
content: string;
|
||||
displayContent?: undefined;
|
||||
}
|
||||
|
||||
// a 'thread' means a chat message history
|
||||
export type ChatThreads = {
|
||||
[id: string]: {
|
||||
id: string; // store the id here too
|
||||
createdAt: string; // ISO string
|
||||
lastModified: string; // ISO string
|
||||
messages: ChatMessage[];
|
||||
};
|
||||
}
|
||||
|
||||
export type ThreadsState = {
|
||||
allThreads: ChatThreads;
|
||||
currentThreadId: string; // intended for internal use only
|
||||
currentStagingSelections: CodeStagingSelection[] | null;
|
||||
}
|
||||
|
||||
export type ThreadStreamState = {
|
||||
[threadId: string]: undefined | {
|
||||
error?: { message: string, fullError: Error | null };
|
||||
messageSoFar?: string;
|
||||
streamingToken?: string;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const newThreadObject = () => {
|
||||
const now = new Date().toISOString()
|
||||
return {
|
||||
id: new Date().getTime().toString(),
|
||||
createdAt: now,
|
||||
lastModified: now,
|
||||
messages: [],
|
||||
} satisfies ChatThreads[string]
|
||||
}
|
||||
|
||||
const THREAD_STORAGE_KEY = 'void.chatThreadStorage'
|
||||
|
||||
export interface IChatThreadService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
readonly state: ThreadsState;
|
||||
readonly streamState: ThreadStreamState;
|
||||
|
||||
onDidChangeCurrentThread: Event<void>;
|
||||
onDidChangeStreamState: Event<{ threadId: string }>
|
||||
|
||||
getCurrentThread(): ChatThreads[string];
|
||||
openNewThread(): void;
|
||||
switchToThread(threadId: string): void;
|
||||
|
||||
setStaging(stagingSelection: CodeStagingSelection[] | null): void;
|
||||
|
||||
addUserMessageAndStreamResponse(userMessage: string): Promise<void>;
|
||||
cancelStreaming(threadId: string): void;
|
||||
dismissStreamError(threadId: string): void;
|
||||
|
||||
}
|
||||
|
||||
export const IChatThreadService = createDecorator<IChatThreadService>('voidChatThreadService');
|
||||
class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
// this fires when the current thread changes at all (a switch of currentThread, or a message added to it, etc)
|
||||
private readonly _onDidChangeCurrentThread = new Emitter<void>();
|
||||
readonly onDidChangeCurrentThread: Event<void> = this._onDidChangeCurrentThread.event;
|
||||
|
||||
readonly streamState: ThreadStreamState = {}
|
||||
private readonly _onDidChangeStreamState = new Emitter<{ threadId: string }>();
|
||||
readonly onDidChangeStreamState: Event<{ threadId: string }> = this._onDidChangeStreamState.event;
|
||||
|
||||
state: ThreadsState // allThreads is persisted, currentThread is not
|
||||
|
||||
constructor(
|
||||
@IStorageService private readonly _storageService: IStorageService,
|
||||
@IModelService private readonly _modelService: IModelService,
|
||||
@ILLMMessageService private readonly _llmMessageService: ILLMMessageService,
|
||||
) {
|
||||
super()
|
||||
|
||||
this.state = {
|
||||
allThreads: this._readAllThreads(),
|
||||
currentThreadId: null as unknown as string, // gets set in startNewThread()
|
||||
currentStagingSelections: null,
|
||||
}
|
||||
|
||||
// always be in a thread
|
||||
this.openNewThread()
|
||||
}
|
||||
|
||||
|
||||
private _readAllThreads(): ChatThreads {
|
||||
// PUT ANY VERSION CHANGE FORMAT CONVERSION CODE HERE
|
||||
const threads = this._storageService.get(THREAD_STORAGE_KEY, StorageScope.APPLICATION)
|
||||
return threads ? JSON.parse(threads) : {}
|
||||
}
|
||||
|
||||
private _storeAllThreads(threads: ChatThreads) {
|
||||
this._storageService.store(THREAD_STORAGE_KEY, JSON.stringify(threads), StorageScope.APPLICATION, StorageTarget.USER)
|
||||
}
|
||||
|
||||
// this should be the only place this.state = ... appears besides constructor
|
||||
private _setState(state: Partial<ThreadsState>, affectsCurrent: boolean) {
|
||||
this.state = {
|
||||
...this.state,
|
||||
...state
|
||||
}
|
||||
if (affectsCurrent)
|
||||
this._onDidChangeCurrentThread.fire()
|
||||
}
|
||||
|
||||
private _setStreamState(threadId: string, state: Partial<NonNullable<ThreadStreamState[string]>>) {
|
||||
this.streamState[threadId] = {
|
||||
...this.streamState[threadId],
|
||||
...state
|
||||
}
|
||||
this._onDidChangeStreamState.fire({ threadId })
|
||||
}
|
||||
|
||||
|
||||
// ---------- streaming ----------
|
||||
|
||||
finishStreaming = (threadId: string, content: string, error?: { message: string, fullError: Error | null }) => {
|
||||
// add assistant's message to chat history, and clear selection
|
||||
const assistantHistoryElt: ChatMessage = { role: 'assistant', content, displayContent: content || null }
|
||||
this._addMessageToThread(threadId, assistantHistoryElt)
|
||||
this._setStreamState(threadId, { messageSoFar: undefined, streamingToken: undefined, error })
|
||||
}
|
||||
|
||||
async addUserMessageAndStreamResponse(userMessage: string) {
|
||||
const threadId = this.getCurrentThread().id
|
||||
|
||||
const currSelns = this.state.currentStagingSelections ?? []
|
||||
const selections = !currSelns ? null : await Promise.all(
|
||||
currSelns.map(async (sel) => ({ ...sel, content: await VSReadFile(this._modelService, sel.fileURI) }))
|
||||
).then(
|
||||
(files) => files.filter(file => file.content !== null) as CodeSelection[]
|
||||
)
|
||||
|
||||
// add user's message to chat history
|
||||
const instructions = userMessage
|
||||
const userHistoryElt: ChatMessage = { role: 'user', content: chat_prompt(instructions, selections), displayContent: instructions, selections: selections }
|
||||
this._addMessageToThread(threadId, userHistoryElt)
|
||||
|
||||
|
||||
this._setStreamState(threadId, { error: undefined })
|
||||
|
||||
const llmCancelToken = this._llmMessageService.sendLLMMessage({
|
||||
logging: { loggingName: 'Chat' },
|
||||
messages: [
|
||||
{ role: 'system', content: chat_systemMessage },
|
||||
...this.getCurrentThread().messages.map(m => ({ role: m.role, content: m.content || '(null)' })),
|
||||
],
|
||||
onText: ({ newText, fullText }) => {
|
||||
this._setStreamState(threadId, { messageSoFar: fullText })
|
||||
},
|
||||
onFinalMessage: ({ fullText: content }) => {
|
||||
this.finishStreaming(threadId, content)
|
||||
},
|
||||
onError: (error) => {
|
||||
this.finishStreaming(threadId, this.streamState[threadId]?.messageSoFar ?? '', error)
|
||||
},
|
||||
useProviderFor: 'Ctrl+L',
|
||||
|
||||
})
|
||||
if (llmCancelToken === null) return
|
||||
this._setStreamState(threadId, { streamingToken: llmCancelToken })
|
||||
|
||||
}
|
||||
|
||||
cancelStreaming(threadId: string) {
|
||||
const llmCancelToken = this.streamState[threadId]?.streamingToken
|
||||
if (llmCancelToken !== undefined) this._llmMessageService.abort(llmCancelToken)
|
||||
this.finishStreaming(threadId, this.streamState[threadId]?.messageSoFar ?? '')
|
||||
}
|
||||
|
||||
dismissStreamError(threadId: string): void {
|
||||
this._setStreamState(threadId, { error: undefined })
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ---------- the rest ----------
|
||||
|
||||
getCurrentThread(): ChatThreads[string] {
|
||||
const state = this.state
|
||||
return state.allThreads[state.currentThreadId];
|
||||
}
|
||||
|
||||
switchToThread(threadId: string) {
|
||||
// console.log('threadId', threadId)
|
||||
// console.log('messages', this.state.allThreads[threadId].messages)
|
||||
this._setState({ currentThreadId: threadId }, true)
|
||||
}
|
||||
|
||||
|
||||
openNewThread() {
|
||||
// if a thread with 0 messages already exists, switch to it
|
||||
const { allThreads: currentThreads } = this.state
|
||||
for (const threadId in currentThreads) {
|
||||
if (currentThreads[threadId].messages.length === 0) {
|
||||
this.switchToThread(threadId)
|
||||
return
|
||||
}
|
||||
}
|
||||
// otherwise, start a new thread
|
||||
const newThread = newThreadObject()
|
||||
|
||||
// update state
|
||||
const newThreads: ChatThreads = {
|
||||
...currentThreads,
|
||||
[newThread.id]: newThread
|
||||
}
|
||||
this._storeAllThreads(newThreads)
|
||||
this._setState({ allThreads: newThreads, currentThreadId: newThread.id }, true)
|
||||
}
|
||||
|
||||
|
||||
_addMessageToThread(threadId: string, message: ChatMessage) {
|
||||
const { allThreads } = this.state
|
||||
|
||||
const oldThread = allThreads[threadId]
|
||||
|
||||
// update state and store it
|
||||
const newThreads = {
|
||||
...allThreads,
|
||||
[oldThread.id]: {
|
||||
...oldThread,
|
||||
lastModified: new Date().toISOString(),
|
||||
messages: [...oldThread.messages, message],
|
||||
}
|
||||
}
|
||||
this._storeAllThreads(newThreads)
|
||||
this._setState({ allThreads: newThreads }, true) // the current thread just changed (it had a message added to it)
|
||||
}
|
||||
|
||||
|
||||
setStaging(stagingSelection: CodeStagingSelection[] | null): void {
|
||||
this._setState({ currentStagingSelections: stagingSelection }, true) // this is a hack for now
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
registerSingleton(IChatThreadService, ChatThreadService, InstantiationType.Eager);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*------------------------------------------------------------------------------------------
|
||||
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
|
||||
*-----------------------------------------------------------------------------------------*/
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* 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';
|
||||
@@ -80,6 +80,7 @@ export class ConsistentItemService extends Disposable {
|
||||
}
|
||||
|
||||
const initializeEditor = (editor: ICodeEditor) => {
|
||||
// if (editor.getModel()?.uri.scheme !== 'file') return // THIS BREAKS THINGS
|
||||
addTabSwitchListeners(editor)
|
||||
addDisposeListener(editor)
|
||||
putItemsOnEditor(editor, editor.getModel()?.uri ?? null)
|
||||
@@ -157,7 +158,6 @@ export class ConsistentItemService extends Disposable {
|
||||
|
||||
|
||||
removeConsistentItemFromURI(consistentItemId: string) {
|
||||
|
||||
if (!(consistentItemId in this.infoOfConsistentItemId))
|
||||
return
|
||||
|
||||
@@ -173,6 +173,7 @@ export class ConsistentItemService extends Disposable {
|
||||
|
||||
// clear
|
||||
this.consistentItemIdsOfURI[uri.fsPath]?.delete(consistentItemId)
|
||||
|
||||
delete this.infoOfConsistentItemId[consistentItemId]
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
|
||||
// eg "bash" -> "shell"
|
||||
export const nameToVscodeLanguage: { [key: string]: string } = {
|
||||
// Web Technologies
|
||||
'html': 'html',
|
||||
'css': 'css',
|
||||
'scss': 'scss',
|
||||
'sass': 'scss',
|
||||
'less': 'less',
|
||||
'javascript': 'typescript',
|
||||
'js': 'typescript', // use more general renderer
|
||||
'jsx': 'typescript',
|
||||
'typescript': 'typescript',
|
||||
'ts': 'typescript',
|
||||
'tsx': 'typescript',
|
||||
'json': 'json',
|
||||
'jsonc': 'json',
|
||||
|
||||
// Programming Languages
|
||||
'python': 'python',
|
||||
'py': 'python',
|
||||
'java': 'java',
|
||||
'cpp': 'cpp',
|
||||
'c++': 'cpp',
|
||||
'c': 'c',
|
||||
'csharp': 'csharp',
|
||||
'cs': 'csharp',
|
||||
'c#': 'csharp',
|
||||
'go': 'go',
|
||||
'golang': 'go',
|
||||
'rust': 'rust',
|
||||
'rs': 'rust',
|
||||
'ruby': 'ruby',
|
||||
'rb': 'ruby',
|
||||
'php': 'php',
|
||||
'shell': 'shell',
|
||||
'bash': 'shell',
|
||||
'sh': 'shell',
|
||||
'zsh': 'shell',
|
||||
|
||||
// Markup and Config
|
||||
'markdown': 'markdown',
|
||||
'md': 'markdown',
|
||||
'xml': 'xml',
|
||||
'svg': 'xml',
|
||||
'yaml': 'yaml',
|
||||
'yml': 'yaml',
|
||||
'ini': 'ini',
|
||||
'toml': 'ini',
|
||||
|
||||
// Database and Query Languages
|
||||
'sql': 'sql',
|
||||
'mysql': 'sql',
|
||||
'postgresql': 'sql',
|
||||
'graphql': 'graphql',
|
||||
'gql': 'graphql',
|
||||
|
||||
// Others
|
||||
'dockerfile': 'dockerfile',
|
||||
'docker': 'dockerfile',
|
||||
'makefile': 'makefile',
|
||||
'plaintext': 'plaintext',
|
||||
'text': 'plaintext'
|
||||
};
|
||||
|
||||
|
||||
|
||||
// eg ".ts" -> "typescript"
|
||||
const fileExtensionToVscodeLanguage: { [key: string]: string } = {
|
||||
// Web
|
||||
'html': 'html',
|
||||
'htm': 'html',
|
||||
'css': 'css',
|
||||
'scss': 'scss',
|
||||
'less': 'less',
|
||||
'js': 'javascript',
|
||||
'jsx': 'javascript',
|
||||
'ts': 'typescript',
|
||||
'tsx': 'typescript',
|
||||
'json': 'json',
|
||||
'jsonc': 'json',
|
||||
|
||||
// Programming Languages
|
||||
'py': 'python',
|
||||
'java': 'java',
|
||||
'cpp': 'cpp',
|
||||
'cc': 'cpp',
|
||||
'c': 'c',
|
||||
'h': 'cpp',
|
||||
'hpp': 'cpp',
|
||||
'cs': 'csharp',
|
||||
'go': 'go',
|
||||
'rs': 'rust',
|
||||
'rb': 'ruby',
|
||||
'php': 'php',
|
||||
'sh': 'shell',
|
||||
'bash': 'shell',
|
||||
'zsh': 'shell',
|
||||
|
||||
// Markup/Config
|
||||
'md': 'markdown',
|
||||
'markdown': 'markdown',
|
||||
'xml': 'xml',
|
||||
'svg': 'xml',
|
||||
'yaml': 'yaml',
|
||||
'yml': 'yaml',
|
||||
'ini': 'ini',
|
||||
'toml': 'ini',
|
||||
|
||||
// Other
|
||||
'sql': 'sql',
|
||||
'graphql': 'graphql',
|
||||
'gql': 'graphql',
|
||||
'dockerfile': 'dockerfile',
|
||||
'docker': 'dockerfile',
|
||||
'mk': 'makefile',
|
||||
|
||||
// Config Files and Dot Files
|
||||
'npmrc': 'ini',
|
||||
'env': 'ini',
|
||||
'gitignore': 'ignore',
|
||||
'dockerignore': 'ignore',
|
||||
'eslintrc': 'json',
|
||||
'babelrc': 'json',
|
||||
'prettierrc': 'json',
|
||||
'stylelintrc': 'json',
|
||||
'editorconfig': 'ini',
|
||||
'htaccess': 'apacheconf',
|
||||
'conf': 'ini',
|
||||
'config': 'ini',
|
||||
|
||||
// Package Files
|
||||
'package': 'json',
|
||||
'package-lock': 'json',
|
||||
'gemfile': 'ruby',
|
||||
'podfile': 'ruby',
|
||||
'rakefile': 'ruby',
|
||||
|
||||
// Build Systems
|
||||
'cmake': 'cmake',
|
||||
'makefile': 'makefile',
|
||||
'gradle': 'groovy',
|
||||
|
||||
// Shell Scripts
|
||||
'bashrc': 'shell',
|
||||
'zshrc': 'shell',
|
||||
'fish': 'shell',
|
||||
|
||||
// Version Control
|
||||
'gitconfig': 'ini',
|
||||
'hgrc': 'ini',
|
||||
'svnconfig': 'ini',
|
||||
|
||||
// Web Server
|
||||
'nginx': 'nginx',
|
||||
|
||||
// Misc Config
|
||||
'properties': 'properties',
|
||||
'cfg': 'ini',
|
||||
'reg': 'ini'
|
||||
};
|
||||
|
||||
|
||||
export function filenameToVscodeLanguage(filename: string): string | undefined {
|
||||
|
||||
const ext = filename.toLowerCase().split('.').pop();
|
||||
if (!ext) return undefined;
|
||||
|
||||
return fileExtensionToVscodeLanguage[ext];
|
||||
}
|
||||
@@ -1,18 +1,169 @@
|
||||
/*------------------------------------------------------------------------------------------
|
||||
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
|
||||
*-----------------------------------------------------------------------------------------*/
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
export const extractCodeFromResult = (result: string) => {
|
||||
class SurroundingsRemover {
|
||||
readonly originalS: string
|
||||
i: number
|
||||
j: number
|
||||
|
||||
// string is s[i...j]
|
||||
|
||||
constructor(s: string) {
|
||||
this.originalS = s
|
||||
this.i = 0
|
||||
this.j = s.length - 1
|
||||
}
|
||||
value() {
|
||||
return this.originalS.substring(this.i, this.j + 1)
|
||||
}
|
||||
|
||||
// returns whether it removed the whole prefix
|
||||
removePrefix = (prefix: string): boolean => {
|
||||
let offset = 0
|
||||
// console.log('prefix', prefix, Math.min(this.j, prefix.length - 1))
|
||||
while (this.i <= this.j && offset <= prefix.length - 1) {
|
||||
if (this.originalS.charAt(this.i) !== prefix.charAt(offset))
|
||||
break
|
||||
offset += 1
|
||||
this.i += 1
|
||||
}
|
||||
return offset === prefix.length
|
||||
}
|
||||
|
||||
// // removes suffix from right to left
|
||||
removeSuffix = (suffix: string): boolean => {
|
||||
// e.g. suffix = <PRE/>, the string is <PRE>hi<P
|
||||
const s = this.value()
|
||||
// for every possible prefix of `suffix`, check if string ends with it
|
||||
for (let len = Math.min(s.length, suffix.length); len >= 1; len -= 1) {
|
||||
if (s.endsWith(suffix.substring(0, len))) { // the end of the string equals a prefix
|
||||
this.j -= len
|
||||
return len === suffix.length
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
// removeSuffix = (suffix: string): boolean => {
|
||||
// let offset = 0
|
||||
|
||||
// while (this.j >= Math.max(this.i, 0)) {
|
||||
// if (this.originalS.charAt(this.j) !== suffix.charAt(suffix.length - 1 - offset))
|
||||
// break
|
||||
// offset += 1
|
||||
// this.j -= 1
|
||||
// }
|
||||
// return offset === suffix.length
|
||||
// }
|
||||
|
||||
removeFromStartUntil = (until: string, alsoRemoveUntilStr: boolean) => {
|
||||
const index = this.originalS.indexOf(until, this.i)
|
||||
|
||||
if (index === -1) {
|
||||
this.i = this.j + 1
|
||||
return false
|
||||
}
|
||||
// console.log('index', index, until.length)
|
||||
|
||||
if (alsoRemoveUntilStr)
|
||||
this.i = index + until.length
|
||||
else
|
||||
this.i = index
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
removeCodeBlock = () => {
|
||||
const pm = this
|
||||
const foundCodeBlock = pm.removePrefix('```')
|
||||
if (!foundCodeBlock) return false
|
||||
|
||||
pm.removeFromStartUntil('\n', true) // language
|
||||
|
||||
const foundCodeBlockEnd = pm.removeSuffix('```')
|
||||
if (!foundCodeBlockEnd) return false
|
||||
|
||||
pm.removeSuffix('\n')
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
actualRecentlyAdded = (recentlyAddedTextLen: number) => {
|
||||
// aaaaaatextaaaaaa{recentlyAdded}
|
||||
// i ^ j
|
||||
// |
|
||||
// recentyAddedIdx
|
||||
const recentlyAddedIdx = this.j - recentlyAddedTextLen + 1
|
||||
return this.originalS.substring(Math.max(this.i, recentlyAddedIdx), this.j + 1)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const extractCodeFromRegular = ({ text, recentlyAddedTextLen }: { text: string, recentlyAddedTextLen: number }): [string, string] => {
|
||||
// Match either:
|
||||
// 1. ```language\n<code>```
|
||||
// 2. ```<code>```
|
||||
const match = result.match(/```(?:\w+\n)?([\s\S]*?)```|```([\s\S]*?)```/);
|
||||
|
||||
if (!match) {
|
||||
return result;
|
||||
}
|
||||
const pm = new SurroundingsRemover(text)
|
||||
|
||||
// Return whichever group matched (non-empty)
|
||||
return match[1] ?? match[2] ?? result;
|
||||
pm.removeCodeBlock()
|
||||
|
||||
const s = pm.value()
|
||||
const actual = pm.actualRecentlyAdded(recentlyAddedTextLen)
|
||||
|
||||
return [s, actual]
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// Ollama has its own FIM, we should not use this if we use that
|
||||
export const extractCodeFromFIM = ({ text, recentlyAddedTextLen, midTag, }: { text: string, recentlyAddedTextLen: number, midTag: string }): [string, string] => {
|
||||
|
||||
/* ------------- summary of the regex -------------
|
||||
[optional ` | `` | ```]
|
||||
(match optional_language_name)
|
||||
[optional strings here]
|
||||
[required <MID> tag]
|
||||
(match the stuff between mid tags)
|
||||
[optional <MID/> tag]
|
||||
[optional ` | `` | ```]
|
||||
*/
|
||||
|
||||
const pm = new SurroundingsRemover(text)
|
||||
|
||||
pm.removeCodeBlock()
|
||||
|
||||
const foundMid = pm.removePrefix(`<${midTag}>`)
|
||||
|
||||
if (foundMid) {
|
||||
pm.removeSuffix(`</${midTag}>`)
|
||||
}
|
||||
const s = pm.value()
|
||||
const actual = pm.actualRecentlyAdded(recentlyAddedTextLen)
|
||||
|
||||
return [s, actual]
|
||||
|
||||
|
||||
// // const regex = /[\s\S]*?(?:`{1,3}\s*([a-zA-Z_]+[\w]*)?[\s\S]*?)?<MID>([\s\S]*?)(?:<\/MID>|`{1,3}|$)/;
|
||||
// const regex = new RegExp(
|
||||
// `[\\s\\S]*?(?:\`{1,3}\\s*([a-zA-Z_]+[\\w]*)?[\\s\\S]*?)?<${midTag}>([\\s\\S]*?)(?:</${midTag}>|\`{1,3}|$)`,
|
||||
// ''
|
||||
// );
|
||||
// const match = text.match(regex);
|
||||
// if (match) {
|
||||
// const [_, languageName, codeBetweenMidTags] = match;
|
||||
// return [languageName, codeBetweenMidTags] as const
|
||||
|
||||
// } else {
|
||||
// return [undefined, extractCodeFromRegular(text)] as const
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*------------------------------------------------------------------------------------------
|
||||
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
|
||||
*-----------------------------------------------------------------------------------------*/
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { diffLines } from '../react/out/diff/index.js'
|
||||
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
/*------------------------------------------------------------------------------------------
|
||||
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
|
||||
*-----------------------------------------------------------------------------------------*/
|
||||
|
||||
import { isMacintosh } from '../../../../../base/common/platform.js';
|
||||
|
||||
// import { OperatingSystem, OS } from '../../../../base/common/platform.js';
|
||||
// OS === OperatingSystem.Macintosh
|
||||
export function getCmdKey(): string {
|
||||
if (isMacintosh) {
|
||||
return '⌘';
|
||||
} else {
|
||||
return 'Ctrl';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { URI } from '../../../../../base/common/uri'
|
||||
import { EndOfLinePreference } from '../../../../../editor/common/model'
|
||||
import { IModelService } from '../../../../../editor/common/services/model.js'
|
||||
|
||||
// read files from VSCode
|
||||
export const VSReadFile = async (modelService: IModelService, uri: URI): Promise<string | null> => {
|
||||
const model = modelService.getModel(uri)
|
||||
if (!model) return null
|
||||
return model.getValue(EndOfLinePreference.LF)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
/*------------------------------------------------------------------------------------------
|
||||
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
|
||||
*-----------------------------------------------------------------------------------------*/
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-editor .void-sweepIdxBG {
|
||||
background-color: var(--vscode-void-sweepIdxBG);
|
||||
@@ -70,4 +70,99 @@
|
||||
.void-link {
|
||||
color: #3b82f6;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.void-link:hover {
|
||||
opacity: 80%;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.void-scrollable-element::-webkit-scrollbar,
|
||||
.void-scrollable-element *::-webkit-scrollbar {
|
||||
width: 14px !important;
|
||||
height: 14px !important;
|
||||
}
|
||||
|
||||
.void-scrollable-element::-webkit-scrollbar-track,
|
||||
.void-scrollable-element *::-webkit-scrollbar-track {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.void-scrollable-element::-webkit-scrollbar-thumb,
|
||||
.void-scrollable-element *::-webkit-scrollbar-thumb {
|
||||
background-color: transparent !important;
|
||||
border-radius: 0px !important;
|
||||
}
|
||||
|
||||
.void-scrollable-element::-webkit-scrollbar-thumb:hover,
|
||||
.void-scrollable-element *::-webkit-scrollbar-thumb:hover {
|
||||
background-color: var(--vscode-scrollbarSlider-hoverBackground) !important;
|
||||
}
|
||||
|
||||
.void-scrollable-element::-webkit-scrollbar-thumb:active,
|
||||
.void-scrollable-element *::-webkit-scrollbar-thumb:active {
|
||||
background-color: var(--vscode-scrollbarSlider-activeBackground) !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;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
/*------------------------------------------------------------------------------------------
|
||||
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
|
||||
*-----------------------------------------------------------------------------------------*/
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
|
||||
import { CodeSelection } from '../threadHistoryService.js';
|
||||
import { URI } from '../../../../../base/common/uri.js';
|
||||
import { filenameToVscodeLanguage } from '../helpers/detectLanguage.js';
|
||||
import { CodeSelection } from '../chatThreadService.js';
|
||||
|
||||
export const chat_systemMessage = `\
|
||||
You are a coding assistant. You are given a list of relevant files \`files\`, a selection that the user is making \`selection\`, and instructions to follow \`instructions\`.
|
||||
@@ -22,25 +24,25 @@ Instructions:
|
||||
|
||||
FILES
|
||||
selected file \`math.ts\`:
|
||||
\`\`\`
|
||||
\`\`\` typescript
|
||||
const addNumbers = (a, b) => a + b
|
||||
const subtractNumbers = (a, b) => a - b
|
||||
const divideNumbers = (a, b) => a / b
|
||||
\`\`\`
|
||||
|
||||
SELECTION
|
||||
\`\`\`
|
||||
\`\`\` typescript
|
||||
const subtractNumbers = (a, b) => a - b
|
||||
\`\`\`
|
||||
|
||||
INSTRUCTIONS
|
||||
\`\`\`
|
||||
\`\`\` typescript
|
||||
add a function that multiplies numbers below this
|
||||
\`\`\`
|
||||
|
||||
EXPECTED OUTPUT
|
||||
We can add the following code to the file:
|
||||
\`\`\`
|
||||
\`\`\` typescript
|
||||
// existing code...
|
||||
const subtractNumbers = (a, b) => a - b;
|
||||
const multiplyNumbers = (a, b) => a * b;
|
||||
@@ -51,7 +53,7 @@ const multiplyNumbers = (a, b) => a * b;
|
||||
|
||||
FILES
|
||||
selected file \`fib.ts\`:
|
||||
\`\`\`
|
||||
\`\`\` typescript
|
||||
|
||||
const dfs = (root) => {
|
||||
if (!root) return;
|
||||
@@ -66,18 +68,18 @@ const fib = (n) => {
|
||||
\`\`\`
|
||||
|
||||
SELECTION
|
||||
\`\`\`
|
||||
\`\`\` typescript
|
||||
return fib(n - 1) + fib(n - 2)
|
||||
\`\`\`
|
||||
|
||||
INSTRUCTIONS
|
||||
\`\`\`
|
||||
\`\`\` typescript
|
||||
memoize results
|
||||
\`\`\`
|
||||
|
||||
EXPECTED OUTPUT
|
||||
To implement memoization in your Fibonacci function, you can use a JavaScript object to store previously computed results. This will help avoid redundant calculations and improve performance. Here's how you can modify your function:
|
||||
\`\`\`
|
||||
\`\`\` typescript
|
||||
// existing code...
|
||||
const fib = (n, memo = {}) => {
|
||||
if (n < 1) return 1;
|
||||
@@ -100,7 +102,7 @@ const stringifySelections = (selections: CodeSelection[]) => {
|
||||
return selections.map(({ fileURI, content, selectionStr }) =>
|
||||
`\
|
||||
File: ${fileURI.fsPath}
|
||||
\`\`\`
|
||||
\`\`\` ${filenameToVscodeLanguage(fileURI.fsPath) ?? ''}
|
||||
${content // this was the enite file which is foolish
|
||||
}
|
||||
\`\`\`${selectionStr === null ? '' : `
|
||||
@@ -136,7 +138,7 @@ Directions:
|
||||
|
||||
ORIGINAL_FILE
|
||||
\`Sidebar.tsx\`:
|
||||
\`\`\`
|
||||
\`\`\` typescript
|
||||
import React from 'react';
|
||||
import styles from './Sidebar.module.css';
|
||||
|
||||
@@ -172,7 +174,7 @@ export default Sidebar;
|
||||
\`\`\`
|
||||
|
||||
DIFF
|
||||
\`\`\`
|
||||
\`\`\` typescript
|
||||
@@ ... @@
|
||||
-<div className={styles.sidebar}>
|
||||
-<ul>
|
||||
@@ -211,7 +213,7 @@ DIFF
|
||||
\`\`\`
|
||||
|
||||
NEW_FILE
|
||||
\`\`\`
|
||||
\`\`\` typescript
|
||||
import React from 'react';
|
||||
import styles from './Sidebar.module.css';
|
||||
|
||||
@@ -226,7 +228,7 @@ const Sidebar: React.FC<SidebarProps> = ({ items, onItemSelect, onExtraButtonCli
|
||||
\`\`\`
|
||||
|
||||
COMPLETION
|
||||
\`\`\`
|
||||
\`\`\` typescript
|
||||
<div className={styles.sidebar}>
|
||||
<ul>
|
||||
{items.map((item, index) => (
|
||||
@@ -253,10 +255,13 @@ export default Sidebar;\`\`\`
|
||||
|
||||
|
||||
|
||||
export const ctrlLStream_prompt = ({ originalCode, userMessage }: { originalCode: string, userMessage: string }) => {
|
||||
export const ctrlLStream_prompt = ({ originalCode, userMessage, uri }: { originalCode: string, userMessage: string, uri: URI }) => {
|
||||
|
||||
const language = filenameToVscodeLanguage(uri.fsPath) ?? ''
|
||||
|
||||
return `\
|
||||
ORIGINAL_CODE
|
||||
\`\`\`
|
||||
\`\`\` ${language}
|
||||
${originalCode}
|
||||
\`\`\`
|
||||
|
||||
@@ -281,7 +286,7 @@ export const ctrlKStream_prefixAndSuffix = ({ fullFileStr, startLine, endLine }:
|
||||
const fullFileLines = fullFileStr.split('\n')
|
||||
|
||||
// we can optimize this later
|
||||
const MAX_CHARS = 1024
|
||||
const MAX_PREFIX_SUFFIX_CHARS = 20_000
|
||||
/*
|
||||
|
||||
a
|
||||
@@ -302,7 +307,7 @@ export const ctrlKStream_prefixAndSuffix = ({ fullFileStr, startLine, endLine }:
|
||||
// we'll include fullFileLines[i...(startLine-1)-1].join('\n') in the prefix.
|
||||
while (i !== 0) {
|
||||
const newLine = fullFileLines[i - 1]
|
||||
if (newLine.length + 1 + prefix.length <= MAX_CHARS) { // +1 to include the \n
|
||||
if (newLine.length + 1 + prefix.length <= MAX_PREFIX_SUFFIX_CHARS) { // +1 to include the \n
|
||||
prefix = `${newLine}\n${prefix}`
|
||||
i -= 1
|
||||
}
|
||||
@@ -313,7 +318,7 @@ export const ctrlKStream_prefixAndSuffix = ({ fullFileStr, startLine, endLine }:
|
||||
let j = endLine - 1
|
||||
while (j !== fullFileLines.length - 1) {
|
||||
const newLine = fullFileLines[j + 1]
|
||||
if (newLine.length + 1 + suffix.length <= MAX_CHARS) { // +1 to include the \n
|
||||
if (newLine.length + 1 + suffix.length <= MAX_PREFIX_SUFFIX_CHARS) { // +1 to include the \n
|
||||
suffix = `${suffix}\n${newLine}`
|
||||
j += 1
|
||||
}
|
||||
@@ -324,50 +329,54 @@ export const ctrlKStream_prefixAndSuffix = ({ fullFileStr, startLine, endLine }:
|
||||
|
||||
}
|
||||
|
||||
export const ctrlKStream_prompt = ({ selection, prefix, suffix, userMessage }: { selection: string, prefix: string, suffix: string, userMessage: string, }) => {
|
||||
const onlySpeaksFIM = false
|
||||
|
||||
if (onlySpeaksFIM) {
|
||||
const preTag = 'PRE'
|
||||
const sufTag = 'SUF'
|
||||
const midTag = 'MID'
|
||||
return `\
|
||||
<${preTag}>
|
||||
/* Original Selection:
|
||||
${selection}*/
|
||||
/* Instructions:
|
||||
${userMessage}*/
|
||||
${prefix}</${preTag}>
|
||||
<${sufTag}>${suffix}</${sufTag}>
|
||||
<${midTag}>`
|
||||
}
|
||||
// prompt the model on how to do FIM
|
||||
else {
|
||||
const preTag = 'PRE'
|
||||
const sufTag = 'SUF'
|
||||
const midTag = 'MID'
|
||||
return `\
|
||||
Here is the user's original selection:
|
||||
\`\`\`
|
||||
export type FimTagsType = {
|
||||
preTag: string,
|
||||
sufTag: string,
|
||||
midTag: string
|
||||
}
|
||||
export const defaultFimTags: FimTagsType = {
|
||||
preTag: 'BEFORE',
|
||||
sufTag: 'AFTER',
|
||||
midTag: 'SELECTION',
|
||||
}
|
||||
|
||||
export const ctrlKStream_prompt = ({ selection, prefix, suffix, userMessage, fimTags, isOllamaFIM, language }:
|
||||
{
|
||||
selection: string, prefix: string, suffix: string, userMessage: string, fimTags: FimTagsType, language: string,
|
||||
isOllamaFIM: false, // we require this be false for clarity
|
||||
}) => {
|
||||
const { preTag, sufTag, midTag } = fimTags
|
||||
|
||||
// prompt the model artifically on how to do FIM
|
||||
// const preTag = 'BEFORE'
|
||||
// const sufTag = 'AFTER'
|
||||
// const midTag = 'SELECTION'
|
||||
return `\
|
||||
The user is selecting this code as their SELECTION:
|
||||
\`\`\` ${language}
|
||||
<${midTag}>${selection}</${midTag}>
|
||||
\`\`\`
|
||||
|
||||
The user wants to apply the following instructions to the selection:
|
||||
The user wants to apply the following INSTRUCTIONS to the SELECTION:
|
||||
${userMessage}
|
||||
|
||||
Please rewrite the selection following the user's instructions.
|
||||
Please edit the SELECTION following the user's INSTRUCTIONS, and return the edited selection.
|
||||
|
||||
Instructions to follow:
|
||||
1. Follow the user's instructions
|
||||
2. You may ONLY CHANGE the selection, and nothing else in the file
|
||||
3. Make sure all brackets in the new selection are balanced the same was as in the original selection
|
||||
3. Be careful not to duplicate or remove variables, comments, or other syntax by mistake
|
||||
Note that the SELECTION has code that comes before it. This code is indicated with <${preTag}>...before</${preTag}>.
|
||||
Note also that the SELECTION has code that comes after it. This code is indicated with <${sufTag}>...after</${sufTag}>.
|
||||
|
||||
Complete the following:
|
||||
Instructions:
|
||||
1. Your OUTPUT should be a SINGLE PIECE OF CODE of the form <${midTag}>...new_selection</${midTag}>. Do NOT output any text or explanations before or after this.
|
||||
2. You may ONLY CHANGE the original SELECTION, and NOT the content in the <${preTag}>...</${preTag}> or <${sufTag}>...</${sufTag}> tags.
|
||||
3. Make sure all brackets in the new selection are balanced the same as in the original selection.
|
||||
4. Be careful not to duplicate or remove variables, comments, or other syntax by mistake.
|
||||
|
||||
Given the code:
|
||||
<${preTag}>${prefix}</${preTag}>
|
||||
<${sufTag}>${suffix}</${sufTag}>
|
||||
<${midTag}>`
|
||||
}
|
||||
|
||||
Return only the completion block of code (of the form \`\`\` ${language}\n <${midTag}>...new_selection</${midTag}>\`\`\`):`
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*------------------------------------------------------------------------------------------
|
||||
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
|
||||
*-----------------------------------------------------------------------------------------*/
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* 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 { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js';
|
||||
@@ -10,14 +10,17 @@ import { KeybindingWeight } from '../../../../platform/keybinding/common/keybind
|
||||
import { IMetricsService } from '../../../../platform/void/common/metricsService.js';
|
||||
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
|
||||
import { IInlineDiffsService } from './inlineDiffsService.js';
|
||||
import { InputBox } from '../../../../base/browser/ui/inputbox/inputBox.js';
|
||||
import { roundRangeToLines } from './sidebarActions.js';
|
||||
import { VOID_CTRL_K_ACTION_ID } from './actionIDs.js';
|
||||
import { localize2 } from '../../../../nls.js';
|
||||
|
||||
|
||||
export type QuickEditPropsType = {
|
||||
diffareaid: number,
|
||||
onGetInputBox: (i: InputBox) => void;
|
||||
initStreamingDiffZoneId: number | null,
|
||||
textAreaRef: (ref: HTMLTextAreaElement | null) => void;
|
||||
onChangeHeight: (height: number) => void;
|
||||
onUserUpdateText: (text: string) => void;
|
||||
onChangeText: (text: string) => void;
|
||||
initText: string | null;
|
||||
}
|
||||
|
||||
@@ -30,16 +33,16 @@ export type QuickEdit = {
|
||||
}
|
||||
|
||||
|
||||
export const VOID_CTRL_K_ACTION_ID = 'void.ctrlKAction'
|
||||
registerAction2(class extends Action2 {
|
||||
constructor(
|
||||
) {
|
||||
super({
|
||||
id: VOID_CTRL_K_ACTION_ID,
|
||||
title: 'Void: Quick Edit',
|
||||
f1: true,
|
||||
title: localize2('voidQuickEditAction', 'Void: Quick Edit'),
|
||||
keybinding: {
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KeyK,
|
||||
weight: KeybindingWeight.BuiltinExtension,
|
||||
weight: KeybindingWeight.VoidExtension,
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -48,21 +51,18 @@ registerAction2(class extends Action2 {
|
||||
|
||||
const editorService = accessor.get(ICodeEditorService)
|
||||
const metricsService = accessor.get(IMetricsService)
|
||||
metricsService.capture('User Action', { type: 'Open Ctrl+K' })
|
||||
metricsService.capture('Ctrl+K', {})
|
||||
|
||||
const editor = editorService.getActiveCodeEditor()
|
||||
if (!editor) return;
|
||||
const model = editor.getModel()
|
||||
if (!model) return;
|
||||
const selection = editor.getSelection()
|
||||
const selection = roundRangeToLines(editor.getSelection(), { emptySelectionBehavior: 'line' })
|
||||
if (!selection) return;
|
||||
|
||||
|
||||
const { startLineNumber: startLine, endLineNumber: endLine } = selection
|
||||
|
||||
// deselect - clear selection
|
||||
editor.setSelection({ startLineNumber: startLine, endLineNumber: startLine, startColumn: 1, endColumn: 1 })
|
||||
|
||||
const inlineDiffsService = accessor.get(IInlineDiffsService)
|
||||
inlineDiffsService.addCtrlKZone({ startLine, endLine, editor })
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*------------------------------------------------------------------------------------------
|
||||
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
|
||||
*-----------------------------------------------------------------------------------------*/
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Emitter, Event } from '../../../../base/common/event.js';
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*------------------------------------------------------------------------------------------
|
||||
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
|
||||
*-----------------------------------------------------------------------------------------*/
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { spawn, execSync } from 'child_process';
|
||||
// Added lines below
|
||||
@@ -11,28 +11,69 @@ import { fileURLToPath } from 'url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const __void_name = 'void'
|
||||
|
||||
function doesPathExist(filePath) {
|
||||
try {
|
||||
const stats = fs.statSync(filePath);
|
||||
|
||||
return stats.isFile();
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
return false;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
This function finds `globalDesiredPath` given `localDesiredPath` and `currentPath`
|
||||
|
||||
Diagram:
|
||||
|
||||
...basePath/
|
||||
└── void/
|
||||
├── ...currentPath/ (defined globally)
|
||||
└── ...localDesiredPath/ (defined locally)
|
||||
|
||||
*/
|
||||
function findDesiredPathFromLocalPath(localDesiredPath, currentPath) {
|
||||
|
||||
// walk upwards until currentPath + localDesiredPath exists
|
||||
while (!doesPathExist(path.join(currentPath, localDesiredPath))) {
|
||||
const parentDir = path.dirname(currentPath);
|
||||
|
||||
if (parentDir === currentPath) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
currentPath = parentDir;
|
||||
}
|
||||
|
||||
// return the `globallyDesiredPath`
|
||||
const globalDesiredPath = path.join(currentPath, localDesiredPath)
|
||||
return globalDesiredPath;
|
||||
}
|
||||
|
||||
// hack to refresh styles automatically
|
||||
function saveStylesFile() {
|
||||
setTimeout(() => {
|
||||
try {
|
||||
// Find "void" in __dirname and use that as our base:
|
||||
const voidIdx = __dirname.indexOf(__void_name);
|
||||
const baseDir = __dirname.substring(0, voidIdx + __void_name.length);
|
||||
const target = path.join(
|
||||
baseDir,
|
||||
'src/vs/workbench/contrib/void/browser/react/src2/styles.css'
|
||||
);
|
||||
const pathToCssFile = findDesiredPathFromLocalPath('./src/vs/workbench/contrib/void/browser/react/src2/styles.css', __dirname);
|
||||
|
||||
if (pathToCssFile === undefined) {
|
||||
console.error('[scope-tailwind] Error finding styles.css');
|
||||
return;
|
||||
}
|
||||
|
||||
// Or re-write with the same content:
|
||||
const content = fs.readFileSync(target, 'utf8');
|
||||
fs.writeFileSync(target, content, 'utf8');
|
||||
const content = fs.readFileSync(pathToCssFile, 'utf8');
|
||||
fs.writeFileSync(pathToCssFile, content, 'utf8');
|
||||
console.log('[scope-tailwind] Force-saved styles.css');
|
||||
} catch (err) {
|
||||
console.error('[scope-tailwind] Error saving styles.css:', err);
|
||||
}
|
||||
}, 5000);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
@@ -1,177 +0,0 @@
|
||||
/*------------------------------------------------------------------------------------------
|
||||
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
|
||||
*-----------------------------------------------------------------------------------------*/
|
||||
|
||||
import React, { FormEvent, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useSettingsState, useSidebarState, useThreadsState, useQuickEditState, useAccessor } from '../util/services.js';
|
||||
import { OnError } from '../../../../../../../platform/void/common/llmMessageTypes.js';
|
||||
import { InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js';
|
||||
import { getCmdKey } from '../../../helpers/getCmdKey.js';
|
||||
import { VoidInputBox } from '../util/inputs.js';
|
||||
import { QuickEditPropsType } from '../../../quickEditActions.js';
|
||||
import { ButtonStop, ButtonSubmit } from '../sidebar-tsx/SidebarChat.js';
|
||||
import { ModelDropdown } from '../void-settings-tsx/ModelDropdown.js';
|
||||
import { X } from 'lucide-react';
|
||||
|
||||
export const CtrlKChat = ({ diffareaid, onGetInputBox, onUserUpdateText, onChangeHeight, initText }: QuickEditPropsType) => {
|
||||
|
||||
const accessor = useAccessor()
|
||||
const inlineDiffsService = accessor.get('IInlineDiffsService')
|
||||
const sizerRef = useRef<HTMLDivElement | null>(null)
|
||||
const inputBoxRef: React.MutableRefObject<InputBox | null> = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
const inputContainer = sizerRef.current
|
||||
if (!inputContainer) return;
|
||||
|
||||
// only observing 1 element
|
||||
let resizeObserver: ResizeObserver | undefined
|
||||
resizeObserver = new ResizeObserver((entries) => {
|
||||
const height = entries[0].borderBoxSize[0].blockSize
|
||||
onChangeHeight(height)
|
||||
})
|
||||
resizeObserver.observe(inputContainer);
|
||||
|
||||
return () => { resizeObserver?.disconnect(); };
|
||||
}, [onChangeHeight]);
|
||||
|
||||
// state of current message
|
||||
const [instructions, setInstructions] = useState(initText ?? '') // the user's instructions
|
||||
const onChangeText = useCallback((newStr: string) => {
|
||||
setInstructions(newStr)
|
||||
onUserUpdateText(newStr)
|
||||
}, [setInstructions])
|
||||
const isDisabled = !instructions.trim()
|
||||
|
||||
const currentlyStreamingIdRef = useRef<number | undefined>(undefined)
|
||||
const [isStreaming, setIsStreaming] = useState(false)
|
||||
|
||||
const onSubmit = useCallback((e: FormEvent) => {
|
||||
if (currentlyStreamingIdRef.current !== undefined) return
|
||||
inputBoxRef.current?.disable()
|
||||
|
||||
currentlyStreamingIdRef.current = inlineDiffsService.startApplying({
|
||||
featureName: 'Ctrl+K',
|
||||
diffareaid: diffareaid,
|
||||
userMessage: instructions,
|
||||
})
|
||||
setIsStreaming(true)
|
||||
}, [inlineDiffsService, diffareaid, instructions])
|
||||
|
||||
const onInterrupt = useCallback(() => {
|
||||
if (currentlyStreamingIdRef.current !== undefined)
|
||||
inlineDiffsService.interruptStreaming(currentlyStreamingIdRef.current)
|
||||
inputBoxRef.current?.enable()
|
||||
setIsStreaming(false)
|
||||
}, [inlineDiffsService])
|
||||
|
||||
|
||||
// sync init value
|
||||
const alreadySetRef = useRef(false)
|
||||
useEffect(() => {
|
||||
if (!inputBoxRef.current) return
|
||||
if (alreadySetRef.current) return
|
||||
alreadySetRef.current = true
|
||||
inputBoxRef.current.value = instructions
|
||||
}, [initText, instructions])
|
||||
|
||||
return <div ref={sizerRef} className='py-2 w-full max-w-xl'>
|
||||
<form
|
||||
// copied from SidebarChat.tsx
|
||||
className={`
|
||||
flex flex-col gap-2 py-1 px-2 relative input text-left shrink-0
|
||||
transition-all duration-200
|
||||
rounded-md
|
||||
bg-vscode-input-bg
|
||||
border border-vscode-commandcenter-inactive-border focus-within:border-vscode-commandcenter-active-border hover:border-vscode-commandcenter-active-border
|
||||
`
|
||||
}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
onSubmit(e)
|
||||
return
|
||||
}
|
||||
}}
|
||||
onSubmit={(e) => {
|
||||
if (isDisabled) {
|
||||
// __TODO__ show disabled
|
||||
return
|
||||
}
|
||||
console.log('submit!')
|
||||
onSubmit(e)
|
||||
}}
|
||||
onClick={(e) => {
|
||||
inputBoxRef.current?.focus()
|
||||
}}
|
||||
>
|
||||
|
||||
{/* // this div is used to position the input box properly */}
|
||||
<div
|
||||
className={`w-full z-[999] relative
|
||||
@@[&_textarea]:!void-bg-transparent
|
||||
@@[&_textarea]:!void-outline-none
|
||||
@@[&_textarea]:!void-text-vscode-input-fg
|
||||
@@[&_div.monaco-inputbox]:!void-border-none
|
||||
@@[&_div.monaco-inputbox]:!void-outline-none`}
|
||||
>
|
||||
<div className='flex flex-row justify-between items-end gap-1'>
|
||||
<div className='absolute size-0.5 top-0 right-4 z-[1]'>
|
||||
<X
|
||||
onClick={() => { inlineDiffsService.removeCtrlKZone({ diffareaid }) }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* input */}
|
||||
<div // copied from SidebarChat.tsx
|
||||
className={`w-full
|
||||
@@[&_textarea]:!void-bg-transparent @@[&_textarea]:!void-outline-none @@[&_textarea]:!void-text-vscode-input-fg @@[&_div.monaco-inputbox]:!void-outline-none`}>
|
||||
{/* text input */}
|
||||
<VoidInputBox
|
||||
placeholder={`${getCmdKey()}+K to select`}
|
||||
onChangeText={onChangeText}
|
||||
onCreateInstance={useCallback((instance: InputBox) => {
|
||||
inputBoxRef.current = instance;
|
||||
onGetInputBox(instance);
|
||||
instance.focus()
|
||||
}, [onGetInputBox])}
|
||||
multiline={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
{/* bottom row */}
|
||||
<div
|
||||
className='flex flex-row justify-between items-end gap-1'
|
||||
>
|
||||
{/* submit options */}
|
||||
<div className='max-w-[150px]
|
||||
@@[&_select]:!void-border-none
|
||||
@@[&_select]:!void-outline-none'
|
||||
>
|
||||
<ModelDropdown featureName='Ctrl+K' />
|
||||
</div>
|
||||
|
||||
{/* submit / stop button */}
|
||||
{isStreaming ?
|
||||
// stop button
|
||||
<ButtonStop
|
||||
onClick={onInterrupt}
|
||||
/>
|
||||
:
|
||||
// submit button (up arrow)
|
||||
<ButtonSubmit
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
/*------------------------------------------------------------------------------------------
|
||||
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
|
||||
*-----------------------------------------------------------------------------------------*/
|
||||
|
||||
import { mountFnGenerator } from '../util/mountFnGenerator.js'
|
||||
import { CtrlK } from './CtrlK.js'
|
||||
|
||||
|
||||
export const mountCtrlK = mountFnGenerator(CtrlK)
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*------------------------------------------------------------------------------------------
|
||||
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
|
||||
*-----------------------------------------------------------------------------------------*/
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { diffLines, Change } from 'diff';
|
||||
|
||||
|
||||
@@ -1,87 +1,30 @@
|
||||
/*------------------------------------------------------------------------------------------
|
||||
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
|
||||
*-----------------------------------------------------------------------------------------*/
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ReactNode } from "react"
|
||||
import { VoidCodeEditor } from '../util/inputs.js';
|
||||
import React from 'react';
|
||||
|
||||
import { VoidCodeEditor, VoidCodeEditorProps } from '../util/inputs.js';
|
||||
|
||||
|
||||
const extensionMap: { [key: string]: string } = {
|
||||
// Web
|
||||
'html': 'html',
|
||||
'htm': 'html',
|
||||
'css': 'css',
|
||||
'scss': 'scss',
|
||||
'less': 'less',
|
||||
'js': 'javascript',
|
||||
'jsx': 'javascript',
|
||||
'ts': 'typescript',
|
||||
'tsx': 'typescript',
|
||||
'json': 'json',
|
||||
'jsonc': 'json',
|
||||
export const BlockCode = ({ buttonsOnHover, ...codeEditorProps }: { buttonsOnHover?: React.ReactNode } & VoidCodeEditorProps) => {
|
||||
const isSingleLine = !codeEditorProps.initValue.includes('\n')
|
||||
|
||||
// Programming Languages
|
||||
'py': 'python',
|
||||
'java': 'java',
|
||||
'cpp': 'cpp',
|
||||
'cc': 'cpp',
|
||||
'h': 'cpp',
|
||||
'hpp': 'cpp',
|
||||
'cs': 'csharp',
|
||||
'go': 'go',
|
||||
'rs': 'rust',
|
||||
'rb': 'ruby',
|
||||
'php': 'php',
|
||||
'sh': 'shell',
|
||||
'bash': 'shell',
|
||||
'zsh': 'shell',
|
||||
return (
|
||||
<>
|
||||
<div className="relative group w-full overflow-hidden">
|
||||
{buttonsOnHover === null ? null : (
|
||||
<div className={`z-[1] absolute top-0 right-0 opacity-0 group-hover:opacity-100 duration-200 ${isSingleLine ? 'h-full flex items-center' : ''
|
||||
}`}>
|
||||
<div className={`flex space-x-1 ${isSingleLine ? 'pr-2' : 'p-2'}`}>
|
||||
{buttonsOnHover}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
// Markup/Config
|
||||
'md': 'markdown',
|
||||
'markdown': 'markdown',
|
||||
'xml': 'xml',
|
||||
'svg': 'xml',
|
||||
'yaml': 'yaml',
|
||||
'yml': 'yaml',
|
||||
'ini': 'ini',
|
||||
'toml': 'ini',
|
||||
|
||||
// Other
|
||||
'sql': 'sql',
|
||||
'graphql': 'graphql',
|
||||
'gql': 'graphql',
|
||||
'dockerfile': 'dockerfile',
|
||||
'docker': 'dockerfile'
|
||||
};
|
||||
|
||||
export function getLanguageFromFileName(fileName: string): string {
|
||||
|
||||
const ext = fileName.toLowerCase().split('.').pop();
|
||||
if (!ext) return 'plaintext';
|
||||
|
||||
return extensionMap[ext] || 'plaintext';
|
||||
}
|
||||
|
||||
export const BlockCode = ({ text, buttonsOnHover, language }: { text: string, buttonsOnHover?: ReactNode, language?: string }) => {
|
||||
|
||||
|
||||
const isSingleLine = !text.includes('\n')
|
||||
|
||||
return (<>
|
||||
<div className={`relative group w-full bg-vscode-editor-bg overflow-hidden isolate`}>
|
||||
{buttonsOnHover === null ? null : (
|
||||
<div className="z-[1] absolute top-0 right-0 opacity-0 group-hover:opacity-100 duration-200">
|
||||
<div className={`flex space-x-2 ${isSingleLine ? '' : 'p-2'}`}>{buttonsOnHover}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<VoidCodeEditor
|
||||
initValue={text}
|
||||
language={language}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
<VoidCodeEditor {...codeEditorProps} />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
/*------------------------------------------------------------------------------------------
|
||||
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
|
||||
*-----------------------------------------------------------------------------------------*/
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import React, { JSX, useCallback, useEffect, useState } from 'react'
|
||||
import { marked, MarkedToken, Token } from 'marked'
|
||||
import { BlockCode } from './BlockCode.js'
|
||||
import { useAccessor } from '../util/services.js'
|
||||
import { nameToVscodeLanguage } from '../../../helpers/detectLanguage.js'
|
||||
|
||||
|
||||
enum CopyButtonState {
|
||||
@@ -23,6 +24,8 @@ const CodeButtonsOnHover = ({ text }: { text: string }) => {
|
||||
const [copyButtonState, setCopyButtonState] = useState(CopyButtonState.Copy)
|
||||
const inlineDiffService = accessor.get('IInlineDiffsService')
|
||||
const clipboardService = accessor.get('IClipboardService')
|
||||
const metricsService = accessor.get('IMetricsService')
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
if (copyButtonState !== CopyButtonState.Copy) {
|
||||
@@ -36,6 +39,8 @@ const CodeButtonsOnHover = ({ text }: { text: string }) => {
|
||||
clipboardService.writeText(text)
|
||||
.then(() => { setCopyButtonState(CopyButtonState.Copied) })
|
||||
.catch(() => { setCopyButtonState(CopyButtonState.Error) })
|
||||
metricsService.capture('Copy Code', { length: text.length }) // capture the length only
|
||||
|
||||
}, [text, clipboardService])
|
||||
|
||||
const onApply = useCallback(() => {
|
||||
@@ -43,20 +48,21 @@ const CodeButtonsOnHover = ({ text }: { text: string }) => {
|
||||
featureName: 'Ctrl+L',
|
||||
userMessage: text,
|
||||
})
|
||||
metricsService.capture('Apply Code', { length: text.length }) // capture the length only
|
||||
}, [inlineDiffService])
|
||||
|
||||
const isSingleLine = !text.includes('\n')
|
||||
|
||||
return <>
|
||||
<button
|
||||
className={`${isSingleLine ? '' : 'p-1'} text-xs hover:brightness-110 bg-vscode-input-bg border border-vscode-input-border rounded text-xs text-vscode-input-fg`}
|
||||
className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-1 text-void-fg-1 hover:brightness-110 border border-vscode-input-border rounded`}
|
||||
onClick={onCopy}
|
||||
>
|
||||
{copyButtonState}
|
||||
</button>
|
||||
<button
|
||||
// btn btn-secondary btn-sm border text-xs text-vscode-input-fg border-vscode-input-border rounded
|
||||
className={`${isSingleLine ? '' : 'p-1'} text-xs hover:brightness-110 bg-vscode-input-bg border border-vscode-input-border rounded text-xs text-vscode-input-fg`}
|
||||
// btn btn-secondary btn-sm border text-sm border-vscode-input-border rounded
|
||||
className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-1 text-void-fg-1 hover:brightness-110 border border-vscode-input-border rounded`}
|
||||
onClick={onApply}
|
||||
>
|
||||
Apply
|
||||
@@ -64,8 +70,21 @@ const CodeButtonsOnHover = ({ text }: { text: string }) => {
|
||||
</>
|
||||
}
|
||||
|
||||
export const CodeSpan = ({ children, className }: { children: React.ReactNode, className?: string }) => {
|
||||
return <code className={`
|
||||
bg-void-bg-1
|
||||
px-1
|
||||
rounded-sm
|
||||
font-mono font-medium
|
||||
break-all
|
||||
${className}
|
||||
`}
|
||||
>
|
||||
{children}
|
||||
</code>
|
||||
}
|
||||
|
||||
const RenderToken = ({ token, nested = false }: { token: Token | string, nested?: boolean }): JSX.Element => {
|
||||
const RenderToken = ({ token, nested = false, noSpace = false }: { token: Token | string, nested?: boolean, noSpace?: boolean }): JSX.Element => {
|
||||
|
||||
// deal with built-in tokens first (assume marked token)
|
||||
const t = token as MarkedToken
|
||||
@@ -76,53 +95,68 @@ const RenderToken = ({ token, nested = false }: { token: Token | string, nested?
|
||||
|
||||
if (t.type === "code") {
|
||||
return <BlockCode
|
||||
text={t.text}
|
||||
// language={t.lang} // instead use vscode to detect language
|
||||
initValue={t.text}
|
||||
language={t.lang === undefined ? undefined : nameToVscodeLanguage[t.lang]} // use vscode to detect language
|
||||
buttonsOnHover={<CodeButtonsOnHover text={t.text} />}
|
||||
/>
|
||||
}
|
||||
|
||||
if (t.type === "heading") {
|
||||
const HeadingTag = `h${t.depth}` as keyof JSX.IntrinsicElements
|
||||
return <HeadingTag>{t.text}</HeadingTag>
|
||||
const headingClasses: { [h: string]: string } = {
|
||||
h1: "text-4xl font-semibold mt-6 mb-4 pb-2 border-b border-void-bg-2",
|
||||
h2: "text-3xl font-semibold mt-6 mb-4 pb-2 border-b border-void-bg-2",
|
||||
h3: "text-2xl font-semibold mt-6 mb-4",
|
||||
h4: "text-xl font-semibold mt-6 mb-4",
|
||||
h5: "text-lg font-semibold mt-6 mb-4",
|
||||
h6: "text-base font-semibold mt-6 mb-4 text-gray-600"
|
||||
}
|
||||
return <HeadingTag className={headingClasses[HeadingTag]}>{t.text}</HeadingTag>
|
||||
}
|
||||
|
||||
if (t.type === "table") {
|
||||
return (
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
{t.header.map((cell: any, index: number) => (
|
||||
<th key={index} style={{ textAlign: t.align[index] || "left" }}>
|
||||
{cell.raw}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{t.rows.map((row: any[], rowIndex: number) => (
|
||||
<tr key={rowIndex}>
|
||||
{row.map((cell: any, cellIndex: number) => (
|
||||
<td
|
||||
key={cellIndex}
|
||||
style={{ textAlign: t.align[cellIndex] || "left" }}
|
||||
<div className={`${noSpace ? '' : 'my-4'} overflow-x-auto`}>
|
||||
<table className="min-w-full border border-void-bg-2">
|
||||
<thead>
|
||||
<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" }}
|
||||
>
|
||||
{cell.raw}
|
||||
</td>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody>
|
||||
{t.rows.map((row: any[], rowIndex: number) => (
|
||||
<tr key={rowIndex} className={rowIndex % 2 === 0 ? 'bg-white' : 'bg-void-bg-1'}>
|
||||
{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" }}
|
||||
>
|
||||
{cell.raw}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (t.type === "hr") {
|
||||
return <hr />
|
||||
return <hr className="my-6 border-t border-void-bg-2" />
|
||||
}
|
||||
|
||||
if (t.type === "blockquote") {
|
||||
return <blockquote>{t.text}</blockquote>
|
||||
return <blockquote className={`pl-4 border-l-4 border-void-bg-2 italic ${noSpace ? '' : 'my-4'}`}>{t.text}</blockquote>
|
||||
}
|
||||
|
||||
if (t.type === "list") {
|
||||
@@ -130,14 +164,16 @@ const RenderToken = ({ token, nested = false }: { token: Token | string, nested?
|
||||
return (
|
||||
<ListTag
|
||||
start={t.start ? t.start : undefined}
|
||||
className={`list-inside ${t.ordered ? "list-decimal" : "list-disc"}`}
|
||||
className={`list-inside pl-2 ${noSpace ? '' : 'my-4'} ${t.ordered ? "list-decimal" : "list-disc"}`}
|
||||
>
|
||||
{t.items.map((item, index) => (
|
||||
<li key={index}>
|
||||
<li key={index} className={`${noSpace ? '' : 'mb-4'}`}>
|
||||
{item.task && (
|
||||
<input type="checkbox" checked={item.checked} readOnly />
|
||||
<input type="checkbox" checked={item.checked} readOnly className="mr-2 form-checkbox" />
|
||||
)}
|
||||
<ChatMarkdownRender string={item.text} nested={true} />
|
||||
<span className="ml-1">
|
||||
<ChatMarkdownRender string={item.text} nested={true} />
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ListTag>
|
||||
@@ -152,13 +188,12 @@ const RenderToken = ({ token, nested = false }: { token: Token | string, nested?
|
||||
</>
|
||||
if (nested)
|
||||
return contents
|
||||
return <p>{contents}</p>
|
||||
return <p className={`${noSpace ? '' : 'my-4'} leading`}>{contents}</p>
|
||||
}
|
||||
|
||||
// don't actually render <html> tags, just render strings of them
|
||||
if (t.type === "html") {
|
||||
return (
|
||||
<pre>
|
||||
<pre className={`bg-4oid-bg-2 p-4 rounded-lg ${noSpace ? '' : 'my-4'} font-mono text-sm`}>
|
||||
{`<html>`}
|
||||
{t.raw}
|
||||
{`</html>`}
|
||||
@@ -176,30 +211,40 @@ const RenderToken = ({ token, nested = false }: { token: Token | string, nested?
|
||||
|
||||
if (t.type === "link") {
|
||||
return (
|
||||
<a className='underline' onClick={() => { window.open(t.href) }} href={t.href} title={t.title ?? undefined}>
|
||||
<a
|
||||
className='underline'
|
||||
onClick={() => { window.open(t.href) }}
|
||||
href={t.href}
|
||||
title={t.title ?? undefined}
|
||||
>
|
||||
{t.text}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
if (t.type === "image") {
|
||||
return <img src={t.href} alt={t.text} title={t.title ?? undefined} />
|
||||
return <img
|
||||
src={t.href}
|
||||
alt={t.text}
|
||||
title={t.title ?? undefined}
|
||||
className={`max4w-full h-auto rounded ${noSpace ? '' : 'my-4'}`}
|
||||
/>
|
||||
}
|
||||
|
||||
if (t.type === "strong") {
|
||||
return <strong>{t.text}</strong>
|
||||
return <strong className="font-semibold">{t.text}</strong>
|
||||
}
|
||||
|
||||
if (t.type === "em") {
|
||||
return <em>{t.text}</em>
|
||||
return <em className="italic">{t.text}</em>
|
||||
}
|
||||
|
||||
// inline code
|
||||
if (t.type === "codespan") {
|
||||
return (
|
||||
<code className="text-vscode-text-preformat-fg bg-vscode-text-preformat-bg px-1 rounded-sm font-mono">
|
||||
<CodeSpan>
|
||||
{t.text}
|
||||
</code>
|
||||
</CodeSpan>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -209,24 +254,24 @@ const RenderToken = ({ token, nested = false }: { token: Token | string, nested?
|
||||
|
||||
// strikethrough
|
||||
if (t.type === "del") {
|
||||
return <del>{t.text}</del>
|
||||
return <del className="line-through">{t.text}</del>
|
||||
}
|
||||
|
||||
// default
|
||||
return (
|
||||
<div className="bg-orange-50 rounded-sm overflow-hidden">
|
||||
<span className="text-xs text-orange-500">Unknown type:</span>
|
||||
<div className="bg-orange-50 rounded-sm overflow-hidden p-2">
|
||||
<span className="text-sm text-orange-500">Unknown type:</span>
|
||||
{t.raw}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const ChatMarkdownRender = ({ string, nested = false }: { string: string, nested?: boolean }) => {
|
||||
export const ChatMarkdownRender = ({ string, nested = false, noSpace }: { string: string, nested?: boolean, noSpace?: boolean }) => {
|
||||
const tokens = marked.lexer(string); // https://marked.js.org/using_pro#renderer
|
||||
return (
|
||||
<>
|
||||
{tokens.map((token, index) => (
|
||||
<RenderToken key={index} token={token} nested={nested} />
|
||||
<RenderToken key={index} token={token} nested={nested} noSpace={noSpace} />
|
||||
))}
|
||||
</>
|
||||
)
|
||||
|
||||
+8
-8
@@ -1,21 +1,21 @@
|
||||
/*------------------------------------------------------------------------------------------
|
||||
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
|
||||
*-----------------------------------------------------------------------------------------*/
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useIsDark, useSidebarState } from '../util/services.js'
|
||||
import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js'
|
||||
import { CtrlKChat } from './CtrlKChat.js'
|
||||
import { QuickEditChat } from './QuickEditChat.js'
|
||||
import { QuickEditPropsType } from '../../../quickEditActions.js'
|
||||
|
||||
export const CtrlK = (props: QuickEditPropsType) => {
|
||||
export const QuickEdit = (props: QuickEditPropsType) => {
|
||||
|
||||
const isDark = useIsDark()
|
||||
|
||||
return <div className={`@@void-scope ${isDark ? 'dark' : ''}`}>
|
||||
<ErrorBoundary>
|
||||
<CtrlKChat {...props} />
|
||||
<QuickEditChat {...props} />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import React, { FormEvent, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useSettingsState, useSidebarState, useChatThreadsState, useQuickEditState, useAccessor } from '../util/services.js';
|
||||
import { TextAreaFns, VoidInputBox2 } from '../util/inputs.js';
|
||||
import { QuickEditPropsType } from '../../../quickEditActions.js';
|
||||
import { ButtonStop, ButtonSubmit, IconX } from '../sidebar-tsx/SidebarChat.js';
|
||||
import { ModelDropdown } from '../void-settings-tsx/ModelDropdown.js';
|
||||
import { VOID_CTRL_K_ACTION_ID } from '../../../actionIDs.js';
|
||||
import { useRefState } from '../util/helpers.js';
|
||||
import { useScrollbarStyles } from '../util/useScrollbarStyles.js';
|
||||
|
||||
export const QuickEditChat = ({
|
||||
diffareaid,
|
||||
initStreamingDiffZoneId,
|
||||
onChangeHeight,
|
||||
onChangeText: onChangeText_,
|
||||
textAreaRef: textAreaRef_,
|
||||
initText
|
||||
}: QuickEditPropsType) => {
|
||||
|
||||
const accessor = useAccessor()
|
||||
const inlineDiffsService = accessor.get('IInlineDiffsService')
|
||||
const sizerRef = useRef<HTMLDivElement | null>(null)
|
||||
const textAreaRef = useRef<HTMLTextAreaElement | null>(null)
|
||||
const textAreaFnsRef = useRef<TextAreaFns | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const inputContainer = sizerRef.current
|
||||
if (!inputContainer) return;
|
||||
// only observing 1 element
|
||||
let resizeObserver: ResizeObserver | undefined
|
||||
resizeObserver = new ResizeObserver((entries) => {
|
||||
const height = entries[0].borderBoxSize[0].blockSize
|
||||
onChangeHeight(height)
|
||||
})
|
||||
resizeObserver.observe(inputContainer);
|
||||
return () => { resizeObserver?.disconnect(); };
|
||||
}, [onChangeHeight]);
|
||||
|
||||
|
||||
// state of current message
|
||||
const [instructionsAreEmpty, setInstructionsAreEmpty] = useState(!(initText ?? '')) // the user's instructions
|
||||
const isDisabled = instructionsAreEmpty
|
||||
|
||||
const [currStreamingDiffZoneRef, setCurrentlyStreamingDiffZone] = useRefState<number | null>(initStreamingDiffZoneId)
|
||||
const isStreaming = currStreamingDiffZoneRef.current !== null
|
||||
|
||||
const onSubmit = useCallback((e: FormEvent) => {
|
||||
if (isDisabled) return
|
||||
if (currStreamingDiffZoneRef.current !== null) return
|
||||
textAreaFnsRef.current?.disable()
|
||||
|
||||
const instructions = textAreaRef.current?.value ?? ''
|
||||
const id = inlineDiffsService.startApplying({
|
||||
featureName: 'Ctrl+K',
|
||||
diffareaid: diffareaid,
|
||||
userMessage: instructions,
|
||||
})
|
||||
setCurrentlyStreamingDiffZone(id ?? null)
|
||||
}, [currStreamingDiffZoneRef, setCurrentlyStreamingDiffZone, isDisabled, inlineDiffsService, diffareaid])
|
||||
|
||||
const onInterrupt = useCallback(() => {
|
||||
if (currStreamingDiffZoneRef.current === null) return
|
||||
inlineDiffsService.interruptStreaming(currStreamingDiffZoneRef.current)
|
||||
setCurrentlyStreamingDiffZone(null)
|
||||
textAreaFnsRef.current?.enable()
|
||||
}, [currStreamingDiffZoneRef, setCurrentlyStreamingDiffZone, inlineDiffsService])
|
||||
|
||||
|
||||
const onX = useCallback(() => {
|
||||
onInterrupt()
|
||||
inlineDiffsService.removeCtrlKZone({ diffareaid })
|
||||
}, [inlineDiffsService, diffareaid])
|
||||
|
||||
useScrollbarStyles(sizerRef)
|
||||
|
||||
const keybindingString = accessor.get('IKeybindingService').lookupKeybinding(VOID_CTRL_K_ACTION_ID)?.getLabel()
|
||||
|
||||
return <div ref={sizerRef} style={{ maxWidth: 500 }} className={`py-2 w-full`}>
|
||||
<form
|
||||
// copied from SidebarChat.tsx
|
||||
className={`
|
||||
flex flex-col gap-2 p-2 relative input text-left shrink-0
|
||||
transition-all duration-200
|
||||
rounded-md
|
||||
bg-vscode-input-bg
|
||||
border border-void-border-3 focus-within:border-void-border-1 hover:border-void-border-1
|
||||
`}
|
||||
onClick={(e) => {
|
||||
textAreaRef.current?.focus()
|
||||
}}
|
||||
>
|
||||
|
||||
{/* // this div is used to position the input box properly */}
|
||||
<div
|
||||
className={`w-full z-[999] relative`}
|
||||
>
|
||||
<div className='flex flex-row items-center justify-between items-end gap-1'>
|
||||
|
||||
{/* input */}
|
||||
<div // copied from SidebarChat.tsx
|
||||
className={`w-full`}
|
||||
>
|
||||
{/* text input */}
|
||||
<VoidInputBox2
|
||||
className='px-1'
|
||||
initValue={initText}
|
||||
|
||||
ref={useCallback((r: HTMLTextAreaElement | null) => {
|
||||
textAreaRef.current = r
|
||||
textAreaRef_(r)
|
||||
|
||||
// if presses the esc key, X
|
||||
r?.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape')
|
||||
onX()
|
||||
})
|
||||
|
||||
}, [textAreaRef_, onX])}
|
||||
|
||||
fnsRef={textAreaFnsRef}
|
||||
|
||||
placeholder={`Enter instructions...`}
|
||||
// ${keybindingString} to select.
|
||||
|
||||
onChangeText={useCallback((newStr: string) => {
|
||||
setInstructionsAreEmpty(!newStr)
|
||||
onChangeText_(newStr)
|
||||
}, [onChangeText_])}
|
||||
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
onSubmit(e)
|
||||
return
|
||||
}
|
||||
}}
|
||||
|
||||
multiline={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* X button */}
|
||||
<div className='absolute -top-1 -right-1 cursor-pointer z-1'>
|
||||
<IconX
|
||||
size={16}
|
||||
className="p-[1px] stroke-[2] opacity-80 text-void-fg-3 hover:brightness-95"
|
||||
onClick={onX}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* bottom row */}
|
||||
<div
|
||||
className='flex flex-row justify-between items-end gap-1'
|
||||
>
|
||||
{/* submit options */}
|
||||
<div className='max-w-[150px]
|
||||
@@[&_select]:!void-border-none
|
||||
@@[&_select]:!void-outline-none'
|
||||
>
|
||||
<ModelDropdown featureName='Ctrl+K' />
|
||||
</div>
|
||||
|
||||
{/* submit / stop button */}
|
||||
{isStreaming ?
|
||||
// stop button
|
||||
<ButtonStop
|
||||
onClick={onInterrupt}
|
||||
/>
|
||||
:
|
||||
// submit button (up arrow)
|
||||
<ButtonSubmit
|
||||
onClick={onSubmit}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* 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 { QuickEdit } from './QuickEdit.js'
|
||||
|
||||
|
||||
export const mountCtrlK = mountFnGenerator(QuickEdit)
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*------------------------------------------------------------------------------------------
|
||||
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
|
||||
*-----------------------------------------------------------------------------------------*/
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import React, { Component, ErrorInfo, ReactNode } from 'react';
|
||||
import { ErrorDisplay } from './ErrorDisplay.js';
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
/*------------------------------------------------------------------------------------------
|
||||
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
|
||||
*-----------------------------------------------------------------------------------------*/
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { AlertCircle, ChevronDown, ChevronUp, X } from 'lucide-react';
|
||||
import { errorDetails } from '../../../../../../../platform/void/common/llmMessageTypes.js';
|
||||
|
||||
|
||||
export const ErrorDisplay = ({
|
||||
message,
|
||||
message:message_,
|
||||
fullError,
|
||||
onDismiss,
|
||||
showDismiss,
|
||||
@@ -20,54 +21,46 @@ export const ErrorDisplay = ({
|
||||
}) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
let details: string | null = null;
|
||||
const details = errorDetails(fullError)
|
||||
|
||||
if (fullError === null) {
|
||||
details = null
|
||||
}
|
||||
else if (typeof fullError === 'object') {
|
||||
details = JSON.stringify(fullError, null, 2)
|
||||
}
|
||||
else if (typeof fullError === 'string') {
|
||||
details = null
|
||||
}
|
||||
const message = message_ === 'TypeError: fetch failed' ? 'TypeError: fetch failed. This likely means you specified the wrong endpoint in Void Settings.' : message_
|
||||
|
||||
|
||||
return (
|
||||
<div className={`rounded-lg border border-red-200 bg-red-50 p-4 overflow-auto`}>
|
||||
{/* Header */}
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex gap-3">
|
||||
<AlertCircle className="h-5 w-5 text-red-600 mt-0.5" />
|
||||
<div className="flex-1">
|
||||
<h3 className="font-semibold text-red-800">
|
||||
<div className='flex items-start justify-between'>
|
||||
<div className='flex gap-3'>
|
||||
<AlertCircle className='h-5 w-5 text-red-600 mt-0.5' />
|
||||
<div className='flex-1'>
|
||||
<h3 className='font-semibold text-red-800'>
|
||||
{/* eg Error */}
|
||||
Error
|
||||
</h3>
|
||||
<p className="text-red-700 mt-1">
|
||||
<p className='text-red-700 mt-1'>
|
||||
{/* eg Something went wrong */}
|
||||
{message}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<div className='flex gap-2'>
|
||||
{details && (
|
||||
<button className="text-red-600 hover:text-red-800 p-1 rounded"
|
||||
<button className='text-red-600 hover:text-red-800 p-1 rounded'
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
>
|
||||
{isExpanded ? (
|
||||
<ChevronUp className="h-5 w-5" />
|
||||
<ChevronUp className='h-5 w-5' />
|
||||
) : (
|
||||
<ChevronDown className="h-5 w-5" />
|
||||
<ChevronDown className='h-5 w-5' />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
{showDismiss && onDismiss && (
|
||||
<button className="text-red-600 hover:text-red-800 p-1 rounded"
|
||||
<button className='text-red-600 hover:text-red-800 p-1 rounded'
|
||||
onClick={onDismiss}
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
<X className='h-5 w-5' />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -75,10 +68,10 @@ export const ErrorDisplay = ({
|
||||
|
||||
{/* Expandable Details */}
|
||||
{isExpanded && details && (
|
||||
<div className="mt-4 space-y-3 border-t border-red-200 pt-3 overflow-auto">
|
||||
<div className='mt-4 space-y-3 border-t border-red-200 pt-3 overflow-auto'>
|
||||
<div>
|
||||
<span className="font-semibold text-red-800">Full Error: </span>
|
||||
<pre className="text-red-700">{details}</pre>
|
||||
<span className='font-semibold text-red-800'>Full Error: </span>
|
||||
<pre className='text-red-700'>{details}</pre>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*------------------------------------------------------------------------------------------
|
||||
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
|
||||
*-----------------------------------------------------------------------------------------*/
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { mountFnGenerator } from '../util/mountFnGenerator.js'
|
||||
|
||||
@@ -13,18 +13,26 @@ import { useIsDark, useSidebarState } from '../util/services.js';
|
||||
// import { SidebarChat } from './SidebarChat.js';
|
||||
|
||||
import '../styles.css'
|
||||
import { SidebarThreadSelector } from './SidebarThreadSelector.js';
|
||||
import { SidebarChat } from './SidebarChat.js';
|
||||
import ErrorBoundary from './ErrorBoundary.js';
|
||||
|
||||
export const Sidebar = ({ className }: { className: string }) => {
|
||||
const sidebarState = useSidebarState()
|
||||
const { isHistoryOpen, currentTab: tab } = sidebarState
|
||||
const { currentTab: tab } = sidebarState
|
||||
|
||||
const isDark = useIsDark()
|
||||
// ${isDark ? 'dark' : ''}
|
||||
return <div className={`@@void-scope`} style={{ width: '100%', height: '100%' }}>
|
||||
<div className={`w-full h-full flex flex-col py-2 bg-vscode-sidebar-bg`}>
|
||||
// const isDark = useIsDark()
|
||||
return <div
|
||||
className={`@@void-scope`} // ${isDark ? 'dark' : ''}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
>
|
||||
<div
|
||||
// default background + text styles for sidebar
|
||||
className={`
|
||||
w-full h-full
|
||||
bg-void-bg-2
|
||||
text-void-fg-1
|
||||
`}
|
||||
>
|
||||
|
||||
{/* <span onClick={() => {
|
||||
const tabs = ['chat', 'settings', 'threadSelector']
|
||||
@@ -32,11 +40,11 @@ export const Sidebar = ({ className }: { className: string }) => {
|
||||
sidebarStateService.setState({ currentTab: tabs[(index + 1) % tabs.length] as any })
|
||||
}}>clickme {tab}</span> */}
|
||||
|
||||
<div className={`w-full h-auto mb-2 ${isHistoryOpen ? '' : 'hidden'} ring-2 ring-widget-shadow z-10`}>
|
||||
{/* <div className={`w-full h-auto mb-2 ${isHistoryOpen ? '' : 'hidden'} ring-2 ring-widget-shadow z-10`}>
|
||||
<ErrorBoundary>
|
||||
<SidebarThreadSelector />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
<div className={`w-full h-full ${tab === 'chat' ? '' : 'hidden'}`}>
|
||||
<ErrorBoundary>
|
||||
|
||||
@@ -1,32 +1,38 @@
|
||||
/*------------------------------------------------------------------------------------------
|
||||
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
|
||||
*-----------------------------------------------------------------------------------------*/
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import React, { ButtonHTMLAttributes, FormEvent, FormHTMLAttributes, Fragment, useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
|
||||
import { useAccessor, useThreadsState } from '../util/services.js';
|
||||
import { ChatMessage, CodeSelection, CodeStagingSelection, IThreadHistoryService } from '../../../threadHistoryService.js';
|
||||
import { useAccessor, useSidebarState, useChatThreadsState, useChatThreadsStreamState, useUriState } from '../util/services.js';
|
||||
import { ChatMessage, CodeSelection, CodeStagingSelection } from '../../../chatThreadService.js';
|
||||
|
||||
import { BlockCode, getLanguageFromFileName } from '../markdown/BlockCode.js';
|
||||
import { BlockCode } from '../markdown/BlockCode.js';
|
||||
import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js';
|
||||
import { URI } from '../../../../../../../base/common/uri.js';
|
||||
import { EndOfLinePreference } from '../../../../../../../editor/common/model.js';
|
||||
import { IDisposable } from '../../../../../../../base/common/lifecycle.js';
|
||||
import { ErrorDisplay } from './ErrorDisplay.js';
|
||||
import { OnError, ServiceSendLLMMessageParams } from '../../../../../../../platform/void/common/llmMessageTypes.js';
|
||||
import { getCmdKey } from '../../../helpers/getCmdKey.js'
|
||||
import { HistoryInputBox, InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js';
|
||||
import { VoidInputBox } from '../util/inputs.js';
|
||||
import { ModelDropdown } from '../void-settings-tsx/ModelDropdown.js';
|
||||
import { TextAreaFns, VoidCodeEditorProps, VoidInputBox2 } from '../util/inputs.js';
|
||||
import { ModelDropdown, WarningBox } from '../void-settings-tsx/ModelDropdown.js';
|
||||
import { chat_systemMessage, chat_prompt } from '../../../prompt/prompts.js';
|
||||
import { ISidebarStateService } from '../../../sidebarStateService.js';
|
||||
import { ILLMMessageService } from '../../../../../../../platform/void/common/llmMessageService.js';
|
||||
import { IModelService } from '../../../../../../../editor/common/services/model.js';
|
||||
import { SidebarThreadSelector } from './SidebarThreadSelector.js';
|
||||
import { useScrollbarStyles } from '../util/useScrollbarStyles.js';
|
||||
import { VOID_CTRL_L_ACTION_ID } from '../../../actionIDs.js';
|
||||
import { ArrowBigLeftDash, CopyX, Delete, FileX2, SquareX, X } from 'lucide-react';
|
||||
import { filenameToVscodeLanguage } from '../../../helpers/detectLanguage.js';
|
||||
import { Pencil } from 'lucide-react'
|
||||
import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js';
|
||||
|
||||
|
||||
const IconX = ({ size, className = '', ...props }: { size: number, className?: string } & React.SVGProps<SVGSVGElement>) => {
|
||||
export const IconX = ({ size, className = '', ...props }: { size: number, className?: string } & React.SVGProps<SVGSVGElement>) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
@@ -47,14 +53,13 @@ const IconX = ({ size, className = '', ...props }: { size: number, className?: s
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
const IconArrowUp = ({ size, className = '' }: { size: number, className?: string }) => {
|
||||
return (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
className={className}
|
||||
viewBox="0 0 32 32"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
@@ -62,10 +67,9 @@ const IconArrowUp = ({ size, className = '' }: { size: number, className?: strin
|
||||
fill="black"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M15.1918 8.90615C15.6381 8.45983 16.3618 8.45983 16.8081 8.90615L21.9509 14.049C22.3972 14.4953 22.3972 15.2189 21.9509 15.6652C21.5046 16.1116 20.781 16.1116 20.3347 15.6652L17.1428 12.4734V22.2857C17.1428 22.9169 16.6311 23.4286 15.9999 23.4286C15.3688 23.4286 14.8571 22.9169 14.8571 22.2857V12.4734L11.6652 15.6652C11.2189 16.1116 10.4953 16.1116 10.049 15.6652C9.60265 15.2189 9.60265 14.4953 10.049 14.049L15.1918 8.90615Z"
|
||||
d="M5.293 9.707a1 1 0 010-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 01-1.414 1.414L11 7.414V15a1 1 0 11-2 0V7.414L6.707 9.707a1 1 0 01-1.414 0z"
|
||||
></path>
|
||||
</svg>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
@@ -169,38 +173,40 @@ const useResizeObserver = () => {
|
||||
|
||||
|
||||
type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement>
|
||||
const DEFAULT_BUTTON_SIZE = 20;
|
||||
const DEFAULT_BUTTON_SIZE = 22;
|
||||
export const ButtonSubmit = ({ className, disabled, ...props }: ButtonProps & Required<Pick<ButtonProps, 'disabled'>>) => {
|
||||
|
||||
return <button
|
||||
type='submit'
|
||||
className={`size-[20px] rounded-full shrink-0 grow-0 cursor-pointer
|
||||
${disabled ? 'bg-vscode-disabled-fg' : 'bg-white'}
|
||||
type='button'
|
||||
className={`rounded-full flex-shrink-0 flex-grow-0 flex items-center justify-center
|
||||
${disabled ? 'bg-vscode-disabled-fg cursor-default' : 'bg-white cursor-pointer'}
|
||||
${className}
|
||||
`}
|
||||
{...props}
|
||||
>
|
||||
<IconArrowUp size={DEFAULT_BUTTON_SIZE} className="stroke-[2]" />
|
||||
<IconArrowUp size={DEFAULT_BUTTON_SIZE} className="stroke-[2] p-[2px]" />
|
||||
</button>
|
||||
}
|
||||
|
||||
export const ButtonStop = ({ className, ...props }: ButtonHTMLAttributes<HTMLButtonElement>) => {
|
||||
|
||||
return <button
|
||||
className={`rounded-full bg-white shrink-0 grow-0 cursor-pointer flex items-center justify-center
|
||||
className={`rounded-full flex-shrink-0 flex-grow-0 cursor-pointer flex items-center justify-center
|
||||
bg-white
|
||||
${className}
|
||||
`}
|
||||
type='button'
|
||||
{...props}
|
||||
>
|
||||
<IconSquare size={DEFAULT_BUTTON_SIZE} className="stroke-[2] p-[6px]" />
|
||||
<IconSquare size={DEFAULT_BUTTON_SIZE} className="stroke-[3] p-[7px]" />
|
||||
</button>
|
||||
}
|
||||
|
||||
|
||||
const ScrollToBottomContainer = ({ children, className, style }: { children: React.ReactNode, className?: string, style?: React.CSSProperties }) => {
|
||||
const ScrollToBottomContainer = ({ children, className, style, scrollContainerRef }: { children: React.ReactNode, className?: string, style?: React.CSSProperties, scrollContainerRef: React.MutableRefObject<HTMLDivElement | null> }) => {
|
||||
const [isAtBottom, setIsAtBottom] = useState(true); // Start at bottom
|
||||
const divRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const divRef = scrollContainerRef
|
||||
|
||||
const scrollToBottom = () => {
|
||||
if (divRef.current) {
|
||||
@@ -245,13 +251,6 @@ const ScrollToBottomContainer = ({ children, className, style }: { children: Rea
|
||||
};
|
||||
|
||||
|
||||
// read files from VSCode
|
||||
const VSReadFile = async (modelService: IModelService, uri: URI): Promise<string | null> => {
|
||||
const model = modelService.getModel(uri)
|
||||
if (!model) return null
|
||||
return model.getValue(EndOfLinePreference.LF)
|
||||
}
|
||||
|
||||
|
||||
const getBasename = (pathStr: string) => {
|
||||
// 'unixify' path
|
||||
@@ -261,148 +260,277 @@ const getBasename = (pathStr: string) => {
|
||||
}
|
||||
|
||||
export const SelectedFiles = (
|
||||
{ type, selections, setStaging }:
|
||||
| { type: 'past', selections: CodeSelection[] | null; setStaging?: undefined }
|
||||
| { type: 'staging', selections: CodeStagingSelection[] | null; setStaging: ((files: CodeStagingSelection[]) => void) }
|
||||
{ type, selections, setSelections, showProspectiveSelections }:
|
||||
| { type: 'past', selections: CodeSelection[]; setSelections?: undefined, showProspectiveSelections?: undefined }
|
||||
| { type: 'staging', selections: CodeStagingSelection[]; setSelections: ((newSelections: CodeStagingSelection[]) => void), showProspectiveSelections?: boolean }
|
||||
) => {
|
||||
|
||||
// index -> isOpened
|
||||
const [selectionIsOpened, setSelectionIsOpened] = useState<(boolean)[]>(selections?.map(() => false) ?? [])
|
||||
|
||||
// state for tracking hover on clear all button
|
||||
const [isClearHovered, setIsClearHovered] = useState(false)
|
||||
|
||||
const accessor = useAccessor()
|
||||
const commandService = accessor.get('ICommandService')
|
||||
|
||||
// state for tracking prospective files
|
||||
const { currentUri } = useUriState()
|
||||
const [recentUris, setRecentUris] = useState<URI[]>([])
|
||||
const maxRecentUris = 10
|
||||
const maxProspectiveFiles = 3
|
||||
useEffect(() => { // handle recent files
|
||||
if (!currentUri) return
|
||||
setRecentUris(prev => {
|
||||
const withoutCurrent = prev.filter(uri => uri.fsPath !== currentUri.fsPath) // remove duplicates
|
||||
const withCurrent = [currentUri, ...withoutCurrent]
|
||||
return withCurrent.slice(0, maxRecentUris)
|
||||
})
|
||||
}, [currentUri])
|
||||
let prospectiveSelections: CodeStagingSelection[] = []
|
||||
if (type === 'staging' && showProspectiveSelections) { // handle prospective files
|
||||
// add a prospective file if type === 'staging' and if the user is in a file, and if the file is not selected yet
|
||||
prospectiveSelections = recentUris
|
||||
.filter(uri => !selections.find(s => s.range === null && s.fileURI.fsPath === uri.fsPath))
|
||||
.slice(0, maxProspectiveFiles)
|
||||
.map(uri => ({
|
||||
type: 'File',
|
||||
fileURI: uri,
|
||||
selectionStr: null,
|
||||
range: null,
|
||||
}))
|
||||
}
|
||||
|
||||
const allSelections = [...selections, ...prospectiveSelections]
|
||||
|
||||
if (allSelections.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
!!selections && selections.length !== 0 && (
|
||||
<div
|
||||
className='flex flex-wrap gap-2 text-left'
|
||||
>
|
||||
{selections.map((selection, i) => {
|
||||
<div className='flex items-center flex-wrap text-left relative'>
|
||||
|
||||
const isThisSelectionOpened = !!(selection.selectionStr && selectionIsOpened[i])
|
||||
{allSelections.map((selection, i) => {
|
||||
|
||||
return (
|
||||
<div key={i} // container for `selectionSummary` and `selectionText`
|
||||
className={`${isThisSelectionOpened ? 'w-full' : ''}`}
|
||||
>
|
||||
{/* selection summary */}
|
||||
<div
|
||||
// className="relative rounded rounded-e-2xl flex items-center space-x-2 mx-1 mb-1 disabled:cursor-default"
|
||||
className={`flex items-center gap-1 relative
|
||||
rounded-md p-1
|
||||
const isThisSelectionOpened = !!(selection.selectionStr && selectionIsOpened[i])
|
||||
const isThisSelectionAFile = selection.selectionStr === null
|
||||
const isThisSelectionProspective = i > selections.length - 1
|
||||
|
||||
const thisKey = `${isThisSelectionProspective}-${i}-${selections.length}`
|
||||
|
||||
const selectionHTML = (<div key={thisKey} // container for `selectionSummary` and `selectionText`
|
||||
className={`
|
||||
${isThisSelectionOpened ? 'w-full' : ''}
|
||||
`}
|
||||
>
|
||||
{/* selection summary */}
|
||||
<div // container for item and its delete button (if it's last)
|
||||
className='flex items-center gap-1 mr-0.5 my-0.5'
|
||||
>
|
||||
<div // styled summary box
|
||||
className={`flex items-center gap-0.5 relative
|
||||
px-1
|
||||
w-fit h-fit
|
||||
select-none
|
||||
bg-vscode-editor-bg hover:brightness-95
|
||||
border border-vscode-commandcenter-border rounded-xs
|
||||
text-xs text-vscode-editor-fg text-nowrap
|
||||
`}
|
||||
onClick={() => {
|
||||
${isThisSelectionProspective ? 'bg-void-1 text-void-fg-3 opacity-80' : 'bg-void-bg-3 hover:brightness-95 text-void-fg-1'}
|
||||
text-xs text-nowrap
|
||||
border rounded-sm ${isClearHovered && !isThisSelectionProspective ? 'border-void-border-1' : 'border-void-border-2'} hover:border-void-border-1
|
||||
transition-all duration-150`}
|
||||
onClick={() => {
|
||||
if (isThisSelectionProspective) { // add prospective selection to selections
|
||||
if (type !== 'staging') return; // (never)
|
||||
setSelections([...selections, selection as CodeStagingSelection])
|
||||
|
||||
} else if (isThisSelectionAFile) { // open files
|
||||
commandService.executeCommand('vscode.open', selection.fileURI, {
|
||||
preview: true,
|
||||
// preserveFocus: false,
|
||||
});
|
||||
} else { // show text
|
||||
setSelectionIsOpened(s => {
|
||||
const newS = [...s]
|
||||
newS[i] = !newS[i]
|
||||
return newS
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span className=''>
|
||||
{/* file name */}
|
||||
{getBasename(selection.fileURI.fsPath)}
|
||||
{/* selection range */}
|
||||
{selection.selectionStr !== null ? ` (${selection.range.startLineNumber}-${selection.range.endLineNumber})` : ''}
|
||||
</span>
|
||||
|
||||
{/* X button */}
|
||||
{type === 'staging' &&
|
||||
<span
|
||||
className='
|
||||
cursor-pointer
|
||||
bg-vscode-editorwidget-bg hover:bg-vscode-toolbar-hover-bg
|
||||
rounded-md
|
||||
z-1
|
||||
'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (type !== 'staging') return;
|
||||
setStaging([...selections.slice(0, i), ...selections.slice(i + 1)])
|
||||
setSelectionIsOpened(o => [...o.slice(0, i), ...o.slice(i + 1)])
|
||||
}}
|
||||
>
|
||||
<IconX size={16} className="p-[2px] stroke-[3] text-vscode-toolbar-foreground" />
|
||||
</span>
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
{/* file name */}
|
||||
{getBasename(selection.fileURI.fsPath)}
|
||||
{/* selection range */}
|
||||
{!isThisSelectionAFile ? ` (${selection.range.startLineNumber}-${selection.range.endLineNumber})` : ''}
|
||||
</span>
|
||||
|
||||
{/* X button */}
|
||||
{type === 'staging' && !isThisSelectionProspective &&
|
||||
<span
|
||||
className='cursor-pointer z-1'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation(); // don't open/close selection
|
||||
if (type !== 'staging') return;
|
||||
setSelections([...selections.slice(0, i), ...selections.slice(i + 1)])
|
||||
setSelectionIsOpened(o => [...o.slice(0, i), ...o.slice(i + 1)])
|
||||
}}
|
||||
>
|
||||
<IconX size={10} className="stroke-[2]" />
|
||||
</span>}
|
||||
|
||||
{/* type of selection */}
|
||||
{/* <span className='truncate'>{selection.selectionStr !== null ? 'Selection' : 'File'}</span> */}
|
||||
{/* X button */}
|
||||
{/* {type === 'staging' && // hoveredIdx === i
|
||||
<span className='absolute right-0 top-0 translate-x-[50%] translate-y-[-50%] cursor-pointer bg-white rounded-full border border-vscode-input-border z-1'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (type !== 'staging') return;
|
||||
setStaging([...selections.slice(0, i), ...selections.slice(i + 1)])
|
||||
setSelectionIsOpened(o => [...o.slice(0, i), ...o.slice(i + 1)])
|
||||
}}
|
||||
>
|
||||
<IconX size={16} className="p-[2px] stroke-[3]" />
|
||||
</span>
|
||||
} */}
|
||||
|
||||
</div>
|
||||
{/* selection text */}
|
||||
{isThisSelectionOpened &&
|
||||
<div className='w-full p-1 rounded-sm border-vscode-editor-border'>
|
||||
<BlockCode text={selection.selectionStr!} language={getLanguageFromFileName(selection.fileURI.path)} />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
|
||||
{/* clear all selections button */}
|
||||
{/* {type !== 'staging' || selections.length === 0 || i !== selections.length - 1
|
||||
? null
|
||||
: <div className={`flex items-center ${isThisSelectionOpened ? 'w-full' : ''}`}>
|
||||
<div
|
||||
className='rounded-md'
|
||||
onMouseEnter={() => setIsClearHovered(true)}
|
||||
onMouseLeave={() => setIsClearHovered(false)}
|
||||
>
|
||||
<Delete
|
||||
size={16}
|
||||
className={`stroke-[1]
|
||||
stroke-void-fg-1
|
||||
fill-void-bg-3
|
||||
opacity-40
|
||||
hover:opacity-60
|
||||
transition-all duration-150
|
||||
cursor-pointer
|
||||
`}
|
||||
onClick={() => { setSelections([]) }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
} */}
|
||||
</div>
|
||||
{/* selection text */}
|
||||
{isThisSelectionOpened &&
|
||||
<div
|
||||
className='w-full px-1 rounded-sm border-vscode-editor-border'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation(); // don't focus input box
|
||||
}}
|
||||
>
|
||||
<BlockCode
|
||||
initValue={selection.selectionStr!}
|
||||
language={filenameToVscodeLanguage(selection.fileURI.path)}
|
||||
maxHeight={200}
|
||||
showScrollbars={true}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</div>)
|
||||
|
||||
return <Fragment key={thisKey}>
|
||||
{/* divider between `selections` and `prospectiveSelections` */}
|
||||
{/* {selections.length > 0 && i === selections.length && <div className='w-full'></div>} */}
|
||||
{selectionHTML}
|
||||
</Fragment>
|
||||
|
||||
})}
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
const ChatBubble = ({ chatMessage, isLoading }: {
|
||||
chatMessage: ChatMessage,
|
||||
isLoading?: boolean,
|
||||
}) => {
|
||||
|
||||
const ChatBubble_ = ({ isEditMode, isLoading, children, role }: { role: ChatMessage['role'], children: React.ReactNode, isLoading: boolean, isEditMode: boolean }) => {
|
||||
|
||||
return <div
|
||||
// align chatbubble accoridng to role
|
||||
className={`
|
||||
relative
|
||||
${isEditMode ? 'px-2 w-full max-w-full'
|
||||
: role === 'user' ? `px-2 self-end w-fit max-w-full`
|
||||
: role === 'assistant' ? `px-2 self-start w-full max-w-full` : ''
|
||||
}
|
||||
`}
|
||||
>
|
||||
<div
|
||||
// style chatbubble according to role
|
||||
className={`
|
||||
text-left space-y-2 rounded-lg
|
||||
overflow-x-auto max-w-full
|
||||
${role === 'user' ? 'p-2 bg-void-bg-1 text-void-fg-1' : 'px-2'}
|
||||
`}
|
||||
>
|
||||
{children}
|
||||
{isLoading && <IconLoading className='opacity-50 text-sm' />}
|
||||
</div>
|
||||
|
||||
{/* edit button */}
|
||||
{/* {role === 'user' &&
|
||||
<Pencil
|
||||
size={16}
|
||||
className={`
|
||||
absolute top-0 right-2
|
||||
translate-x-0 -translate-y-0
|
||||
cursor-pointer z-1
|
||||
`}
|
||||
onClick={() => { setIsEditMode(v => !v); }}
|
||||
/>
|
||||
} */}
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
const ChatBubble = ({ chatMessage, isLoading }: { chatMessage: ChatMessage, isLoading?: boolean, }) => {
|
||||
|
||||
const role = chatMessage.role
|
||||
|
||||
if (!chatMessage.displayContent)
|
||||
// edit mode state
|
||||
const [isEditMode, setIsEditMode] = useState(false)
|
||||
|
||||
|
||||
if (!chatMessage.content) { // don't show if empty
|
||||
return null
|
||||
}
|
||||
|
||||
let chatbubbleContents: React.ReactNode
|
||||
|
||||
if (role === 'user') {
|
||||
chatbubbleContents = <>
|
||||
<SelectedFiles type='past' selections={chatMessage.selections} />
|
||||
<SelectedFiles type='past' selections={chatMessage.selections || []} />
|
||||
{chatMessage.displayContent}
|
||||
|
||||
{/* {!isEditMode ? chatMessage.displayContent : <></>} */}
|
||||
{/* edit mode content */}
|
||||
{/* TODO this should be the same input box as in the Sidebar */}
|
||||
{/* <textarea
|
||||
value={editModeText}
|
||||
className={`
|
||||
w-full max-w-full
|
||||
h-auto min-h-[81px] max-h-[500px]
|
||||
bg-void-bg-1 resize-none
|
||||
`}
|
||||
style={{ marginTop: 0 }}
|
||||
hidden={!isEditMode}
|
||||
/> */}
|
||||
|
||||
</>
|
||||
}
|
||||
else if (role === 'assistant') {
|
||||
chatbubbleContents = <ChatMarkdownRender string={chatMessage.displayContent} /> // sectionsHTML
|
||||
chatbubbleContents = <ChatMarkdownRender string={chatMessage.displayContent ?? ''} />
|
||||
}
|
||||
|
||||
return <div
|
||||
// style + align chatbubble accoridng to role
|
||||
className={`p-2 mx-2 text-left space-y-2 rounded-lg max-w-full
|
||||
${role === 'user' ? 'self-end' : 'self-start'}
|
||||
${role === 'user' ? 'bg-vscode-input-bg text-vscode-input-fg' : ''}
|
||||
${role === 'assistant' ? 'w-full' : ''}
|
||||
`}
|
||||
>
|
||||
return <ChatBubble_ role={role} isEditMode={isEditMode} isLoading={!!isLoading}>
|
||||
{chatbubbleContents}
|
||||
{isLoading && <IconLoading className='opacity-50 text-sm' />}
|
||||
</div>
|
||||
</ChatBubble_>
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const SidebarChat = () => {
|
||||
|
||||
const inputBoxRef: React.MutableRefObject<InputBox | null> = useRef(null);
|
||||
const textAreaRef = useRef<HTMLTextAreaElement | null>(null)
|
||||
const textAreaFnsRef = useRef<TextAreaFns | null>(null)
|
||||
|
||||
const accessor = useAccessor()
|
||||
const modelService = accessor.get('IModelService')
|
||||
// const modelService = accessor.get('IModelService')
|
||||
const commandService = accessor.get('ICommandService')
|
||||
|
||||
// ----- HIGHER STATE -----
|
||||
// sidebar state
|
||||
@@ -410,172 +538,117 @@ export const SidebarChat = () => {
|
||||
useEffect(() => {
|
||||
const disposables: IDisposable[] = []
|
||||
disposables.push(
|
||||
sidebarStateService.onDidFocusChat(() => { inputBoxRef.current?.focus() }),
|
||||
sidebarStateService.onDidBlurChat(() => { inputBoxRef.current?.blur() })
|
||||
sidebarStateService.onDidFocusChat(() => { textAreaRef.current?.focus() }),
|
||||
sidebarStateService.onDidBlurChat(() => { textAreaRef.current?.blur() })
|
||||
)
|
||||
return () => disposables.forEach(d => d.dispose())
|
||||
}, [sidebarStateService, inputBoxRef])
|
||||
}, [sidebarStateService, textAreaRef])
|
||||
|
||||
const { isHistoryOpen } = useSidebarState()
|
||||
|
||||
// threads state
|
||||
const threadsState = useThreadsState()
|
||||
const threadsStateService = accessor.get('IThreadHistoryService')
|
||||
const chatThreadsState = useChatThreadsState()
|
||||
const chatThreadsService = accessor.get('IChatThreadService')
|
||||
|
||||
const llmMessageService = accessor.get('ILLMMessageService')
|
||||
const currentThread = chatThreadsService.getCurrentThread()
|
||||
const previousMessages = currentThread?.messages ?? []
|
||||
const selections = chatThreadsState.currentStagingSelections
|
||||
|
||||
// stream state
|
||||
const currThreadStreamState = useChatThreadsStreamState(chatThreadsState.currentThreadId)
|
||||
const isStreaming = !!currThreadStreamState?.streamingToken
|
||||
const latestError = currThreadStreamState?.error
|
||||
const messageSoFar = currThreadStreamState?.messageSoFar
|
||||
|
||||
// ----- SIDEBAR CHAT state (local) -----
|
||||
|
||||
// state of chat
|
||||
const [messageStream, setMessageStream] = useState<string | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const latestRequestIdRef = useRef<string | null>(null)
|
||||
|
||||
const [latestError, setLatestError] = useState<Parameters<OnError>[0] | null>(null)
|
||||
|
||||
|
||||
// state of current message
|
||||
const [instructions, setInstructions] = useState('') // the user's instructions
|
||||
const isDisabled = !instructions.trim()
|
||||
const initVal = ''
|
||||
const [instructionsAreEmpty, setInstructionsAreEmpty] = useState(!initVal)
|
||||
const isDisabled = instructionsAreEmpty
|
||||
|
||||
const [sidebarRef, sidebarDimensions] = useResizeObserver()
|
||||
const [formRef, formDimensions] = useResizeObserver()
|
||||
const [historyRef, historyDimensions] = useResizeObserver()
|
||||
|
||||
// const [formHeight, setFormHeight] = useState(0) // TODO should use resize observer instead
|
||||
// const [sidebarHeight, setSidebarHeight] = useState(0)
|
||||
const onChangeText = useCallback((newStr: string) => { setInstructions(newStr) }, [setInstructions])
|
||||
useScrollbarStyles(sidebarRef)
|
||||
|
||||
|
||||
const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||
const onSubmit = async () => {
|
||||
|
||||
e.preventDefault()
|
||||
if (isDisabled) return
|
||||
if (isLoading) return
|
||||
|
||||
|
||||
|
||||
const currSelns = threadsStateService.state._currentStagingSelections ?? []
|
||||
const selections = !currSelns ? null : await Promise.all(
|
||||
currSelns.map(async (sel) => ({ ...sel, content: await VSReadFile(modelService, sel.fileURI) }))
|
||||
).then(
|
||||
(files) => files.filter(file => file.content !== null) as CodeSelection[]
|
||||
)
|
||||
|
||||
|
||||
// // TODO don't save files to the thread history
|
||||
// const selectedSnippets = currSelns.filter(sel => sel.selectionStr !== null)
|
||||
// const selectedFiles = await Promise.all( // do not add these to the context history
|
||||
// currSelns.filter(sel => sel.selectionStr === null)
|
||||
// .map(async (sel) => ({ ...sel, content: await VSReadFile(modelService, sel.fileURI) }))
|
||||
// ).then(
|
||||
// (files) => files.filter(file => file.content !== null) as CodeSelection[]
|
||||
// )
|
||||
// const contextToSendToLLM = ''
|
||||
// const contextToAddToHistory = ''
|
||||
|
||||
|
||||
// add system message to chat history
|
||||
const systemPromptElt: ChatMessage = { role: 'system', content: chat_systemMessage }
|
||||
threadsStateService.addMessageToCurrentThread(systemPromptElt)
|
||||
|
||||
// add user's message to chat history
|
||||
const userHistoryElt: ChatMessage = { role: 'user', content: chat_prompt(instructions, selections), displayContent: instructions, selections: selections }
|
||||
threadsStateService.addMessageToCurrentThread(userHistoryElt)
|
||||
|
||||
const currentThread = threadsStateService.getCurrentThread(threadsStateService.state) // the the instant state right now, don't wait for the React state
|
||||
if (isStreaming) return
|
||||
|
||||
// send message to LLM
|
||||
setIsLoading(true) // must come before message is sent so onError will work
|
||||
setLatestError(null)
|
||||
if (inputBoxRef.current) {
|
||||
inputBoxRef.current.value = ''; // this triggers onDidChangeText
|
||||
inputBoxRef.current.blur();
|
||||
}
|
||||
const userMessage = textAreaRef.current?.value ?? ''
|
||||
await chatThreadsService.addUserMessageAndStreamResponse(userMessage)
|
||||
|
||||
const object: ServiceSendLLMMessageParams = {
|
||||
logging: { loggingName: 'Chat' },
|
||||
messages: [...(currentThread?.messages ?? []).map(m => ({ role: m.role, content: m.content || '(null)' })),],
|
||||
onText: ({ newText, fullText }) => setMessageStream(fullText),
|
||||
onFinalMessage: ({ fullText: content }) => {
|
||||
console.log('chat: running final message')
|
||||
|
||||
// add assistant's message to chat history, and clear selection
|
||||
const assistantHistoryElt: ChatMessage = { role: 'assistant', content, displayContent: content || null }
|
||||
threadsStateService.addMessageToCurrentThread(assistantHistoryElt)
|
||||
setMessageStream(null)
|
||||
setIsLoading(false)
|
||||
},
|
||||
onError: ({ message, fullError }) => {
|
||||
console.log('chat: running error', message, fullError)
|
||||
|
||||
// add assistant's message to chat history, and clear selection
|
||||
let content = messageStream ?? ''; // just use the current content
|
||||
const assistantHistoryElt: ChatMessage = { role: 'assistant', content, displayContent: content || null, }
|
||||
threadsStateService.addMessageToCurrentThread(assistantHistoryElt)
|
||||
|
||||
setMessageStream('')
|
||||
setIsLoading(false)
|
||||
|
||||
setLatestError({ message, fullError })
|
||||
},
|
||||
featureName: 'Ctrl+L',
|
||||
|
||||
}
|
||||
|
||||
const latestRequestId = llmMessageService.sendLLMMessage(object)
|
||||
latestRequestIdRef.current = latestRequestId
|
||||
|
||||
threadsStateService.setStaging([]) // clear staging
|
||||
|
||||
inputBoxRef.current?.focus() // focus input after submit
|
||||
chatThreadsService.setStaging([]) // clear staging
|
||||
textAreaFnsRef.current?.setValue('')
|
||||
textAreaRef.current?.focus() // focus input after submit
|
||||
|
||||
}
|
||||
|
||||
const onAbort = () => {
|
||||
// abort the LLM call
|
||||
if (latestRequestIdRef.current)
|
||||
llmMessageService.abort(latestRequestIdRef.current)
|
||||
|
||||
// if messageStream was not empty, add it to the history
|
||||
const llmContent = messageStream ?? ''
|
||||
const assistantHistoryElt: ChatMessage = { role: 'assistant', content: llmContent, displayContent: messageStream || null, }
|
||||
threadsStateService.addMessageToCurrentThread(assistantHistoryElt)
|
||||
|
||||
setMessageStream('')
|
||||
setIsLoading(false)
|
||||
|
||||
const threadId = currentThread.id
|
||||
chatThreadsService.cancelStreaming(threadId)
|
||||
}
|
||||
|
||||
const currentThread = threadsStateService.getCurrentThread(threadsState)
|
||||
|
||||
const selections = threadsState._currentStagingSelections
|
||||
|
||||
const previousMessages = currentThread?.messages ?? []
|
||||
|
||||
// const [_test_messages, _set_test_messages] = useState<string[]>([])
|
||||
|
||||
const keybindingString = accessor.get('IKeybindingService').lookupKeybinding(VOID_CTRL_L_ACTION_ID)?.getLabel()
|
||||
|
||||
// scroll to top on thread switch
|
||||
const scrollContainerRef = useRef<HTMLDivElement | null>(null)
|
||||
useEffect(() => {
|
||||
if (isHistoryOpen)
|
||||
scrollContainerRef.current?.scrollTo({ top: 0, left: 0 })
|
||||
}, [isHistoryOpen, currentThread.id])
|
||||
|
||||
return <div
|
||||
ref={sidebarRef}
|
||||
className={`w-full h-full`}
|
||||
>
|
||||
{/* thread selector */}
|
||||
<div ref={historyRef}
|
||||
className={`w-full h-auto ${isHistoryOpen ? '' : 'hidden'} ring-2 ring-widget-shadow ring-inset z-10`}
|
||||
>
|
||||
<SidebarThreadSelector />
|
||||
</div>
|
||||
|
||||
{/* previous messages + current stream */}
|
||||
<ScrollToBottomContainer
|
||||
scrollContainerRef={scrollContainerRef}
|
||||
className={`
|
||||
w-full h-auto
|
||||
flex flex-col gap-0
|
||||
flex flex-col gap-1
|
||||
overflow-x-hidden
|
||||
overflow-y-auto
|
||||
`}
|
||||
style={{ maxHeight: sidebarDimensions.height - formDimensions.height - 30 }}
|
||||
style={{ maxHeight: sidebarDimensions.height - historyDimensions.height - formDimensions.height - 36 }} // the height of the previousMessages is determined by all other heights
|
||||
>
|
||||
{/* previous messages */}
|
||||
{previousMessages.map((message, i) => <ChatBubble key={i} chatMessage={message} />)}
|
||||
{previousMessages.map((message, i) =>
|
||||
<ChatBubble key={i} chatMessage={message} />
|
||||
)}
|
||||
|
||||
{/* message stream */}
|
||||
<ChatBubble chatMessage={{ role: 'assistant', content: messageStream, displayContent: messageStream || null }} isLoading={isLoading} />
|
||||
<ChatBubble chatMessage={{ role: 'assistant', content: messageSoFar ?? '', displayContent: messageSoFar || null }} isLoading={isStreaming} />
|
||||
|
||||
{/* {_test_messages.map((_, i) => <div key={i}>div {i}</div>)}
|
||||
<div>{`totalHeight: ${sidebarHeight - formHeight - 30}`}</div>
|
||||
<div>{`sidebarHeight: ${sidebarHeight}`}</div>
|
||||
<div>{`formHeight: ${formHeight}`}</div>
|
||||
<button type='button' onClick={() => { _set_test_messages(d => [...d, 'asdasdsadasd']) }}>add div</button> */}
|
||||
|
||||
{/* error message */}
|
||||
{latestError === undefined ? null :
|
||||
<div className='px-2'>
|
||||
<ErrorDisplay
|
||||
message={latestError.message}
|
||||
fullError={latestError.fullError}
|
||||
onDismiss={() => { chatThreadsService.dismissStreamError(currentThread.id) }}
|
||||
showDismiss={true}
|
||||
/>
|
||||
|
||||
<WarningBox className='text-sm my-2 pl-4' onClick={() => { commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID) }} text='Open settings' />
|
||||
</div>
|
||||
}
|
||||
|
||||
</ScrollToBottomContainer>
|
||||
|
||||
@@ -584,73 +657,41 @@ export const SidebarChat = () => {
|
||||
<div // this div is used to position the input box properly
|
||||
className={`right-0 left-0 m-2 z-[999] overflow-hidden ${previousMessages.length > 0 ? 'absolute bottom-0' : ''}`}
|
||||
>
|
||||
<form
|
||||
<div
|
||||
ref={formRef}
|
||||
className={`
|
||||
flex flex-col gap-2 p-2 relative input text-left shrink-0
|
||||
flex flex-col gap-1 p-2 relative input text-left shrink-0
|
||||
transition-all duration-200
|
||||
rounded-md
|
||||
bg-vscode-input-bg
|
||||
border border-vscode-commandcenter-inactive-border focus-within:border-vscode-commandcenter-active-border hover:border-vscode-commandcenter-active-border
|
||||
max-h-[80vh] overflow-y-auto
|
||||
border border-void-border-3 focus-within:border-void-border-1 hover:border-void-border-1
|
||||
`}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
onSubmit(e)
|
||||
}
|
||||
}}
|
||||
onSubmit={(e) => {
|
||||
console.log('submit!')
|
||||
onSubmit(e)
|
||||
}}
|
||||
onClick={(e) => {
|
||||
inputBoxRef.current?.focus()
|
||||
textAreaRef.current?.focus()
|
||||
}}
|
||||
>
|
||||
{/* top row */}
|
||||
<>
|
||||
{/* selections */}
|
||||
{(selections && selections.length !== 0) &&
|
||||
<SelectedFiles type='staging' selections={selections} setStaging={threadsStateService.setStaging.bind(threadsStateService)} />
|
||||
}
|
||||
|
||||
{/* error message */}
|
||||
{latestError === null ? null :
|
||||
<ErrorDisplay
|
||||
message={latestError.message}
|
||||
fullError={latestError.fullError}
|
||||
onDismiss={() => { setLatestError(null) }}
|
||||
showDismiss={true}
|
||||
/>
|
||||
}
|
||||
<SelectedFiles type='staging' selections={selections || []} setSelections={chatThreadsService.setStaging.bind(chatThreadsService)} showProspectiveSelections={previousMessages.length === 0} />
|
||||
</>
|
||||
|
||||
{/* middle row */}
|
||||
<div
|
||||
className={
|
||||
// // hack to overwrite vscode styles (generated with this code):
|
||||
// `bg-transparent outline-none text-vscode-input-fg min-h-[81px] max-h-[500px]`
|
||||
// .split(' ')
|
||||
// .map(style => `@@[&_textarea]:!void-${style}`) // apply styles to ancestor textarea elements
|
||||
// .join(' ') +
|
||||
// ` outline-none border-none`
|
||||
// .split(' ')
|
||||
// .map(style => `@@[&_div.monaco-inputbox]:!void-${style}`)
|
||||
// .join(' ');
|
||||
`@@[&_textarea]:!void-bg-transparent
|
||||
@@[&_textarea]:!void-outline-none
|
||||
@@[&_textarea]:!void-text-vscode-input-fg
|
||||
@@[&_textarea]:!void-min-h-[81px]
|
||||
@@[&_textarea]:!void-max-h-[500px]
|
||||
@@[&_div.monaco-inputbox]:!void-border-none
|
||||
@@[&_div.monaco-inputbox]:!void-outline-none`
|
||||
}
|
||||
>
|
||||
<div>
|
||||
|
||||
{/* text input */}
|
||||
<VoidInputBox
|
||||
placeholder={`${getCmdKey()}+L to select`}
|
||||
onChangeText={onChangeText}
|
||||
inputBoxRef={inputBoxRef}
|
||||
<VoidInputBox2
|
||||
className='min-h-[81px] p-1'
|
||||
placeholder={`${keybindingString ? `${keybindingString} to select. ` : ''}Enter instructions...`}
|
||||
onChangeText={useCallback((newStr: string) => { setInstructionsAreEmpty(!newStr) }, [setInstructionsAreEmpty])}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
onSubmit()
|
||||
}
|
||||
}}
|
||||
ref={textAreaRef}
|
||||
fnsRef={textAreaFnsRef}
|
||||
multiline={true}
|
||||
/>
|
||||
</div>
|
||||
@@ -662,13 +703,15 @@ export const SidebarChat = () => {
|
||||
{/* submit options */}
|
||||
<div className='max-w-[150px]
|
||||
@@[&_select]:!void-border-none
|
||||
@@[&_select]:!void-outline-none'
|
||||
@@[&_select]:!void-outline-none
|
||||
flex-grow
|
||||
'
|
||||
>
|
||||
<ModelDropdown featureName='Ctrl+L' />
|
||||
</div>
|
||||
|
||||
{/* submit / stop button */}
|
||||
{isLoading ?
|
||||
{isStreaming ?
|
||||
// stop button
|
||||
<ButtonStop
|
||||
onClick={onAbort}
|
||||
@@ -676,13 +719,14 @@ export const SidebarChat = () => {
|
||||
:
|
||||
// submit button (up arrow)
|
||||
<ButtonSubmit
|
||||
onClick={onSubmit}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div >
|
||||
</div >
|
||||
}
|
||||
|
||||
+82
-55
@@ -1,12 +1,12 @@
|
||||
/*------------------------------------------------------------------------------------------
|
||||
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
|
||||
*-----------------------------------------------------------------------------------------*/
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import React from "react";
|
||||
import { useAccessor, useThreadsState } from '../util/services.js';
|
||||
import { IThreadHistoryService } from '../../../threadHistoryService.js';
|
||||
import { useAccessor, useChatThreadsState } from '../util/services.js';
|
||||
import { ISidebarStateService } from '../../../sidebarStateService.js';
|
||||
import { IconX } from './SidebarChat.js';
|
||||
|
||||
|
||||
const truncate = (s: string) => {
|
||||
@@ -19,75 +19,102 @@ const truncate = (s: string) => {
|
||||
|
||||
|
||||
export const SidebarThreadSelector = () => {
|
||||
const threadsState = useThreadsState()
|
||||
const threadsState = useChatThreadsState()
|
||||
|
||||
const accessor = useAccessor()
|
||||
const threadsStateService = accessor.get('IThreadHistoryService')
|
||||
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 > allThreads![threadId2].lastModified ? -1 : 1)
|
||||
const sortedThreadIds = Object.keys(allThreads ?? {})
|
||||
.sort((threadId1, threadId2) => allThreads![threadId1].lastModified > allThreads![threadId2].lastModified ? -1 : 1)
|
||||
.filter(threadId => allThreads![threadId].messages.length !== 0)
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-1 max-h-[400px] overflow-y-auto">
|
||||
<div className="flex p-2 flex-col gap-y-1 max-h-[400px] overflow-y-auto">
|
||||
|
||||
{/* X button at top right */}
|
||||
<div className="text-right">
|
||||
<button className="btn btn-sm" onClick={() => sidebarStateService.setState({ isHistoryOpen: false })}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
className="size-4"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M6 18 18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
<div className="w-full relative flex justify-center items-center">
|
||||
{/* title */}
|
||||
<h2 className='font-bold text-lg'>{`History`}</h2>
|
||||
{/* X button at top right */}
|
||||
<button
|
||||
type='button'
|
||||
className='absolute top-0 right-0'
|
||||
onClick={() => sidebarStateService.setState({ isHistoryOpen: false })}
|
||||
>
|
||||
<IconX
|
||||
size={16}
|
||||
className="p-[1px] stroke-[2] opacity-80 text-void-fg-3 hover:brightness-95"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* a list of all the past threads */}
|
||||
<div className='flex flex-col gap-y-1 overflow-y-auto'>
|
||||
{sortedThreadIds.map((threadId) => {
|
||||
if (!allThreads)
|
||||
return <>Error: Threads not found.</>
|
||||
const pastThread = allThreads[threadId]
|
||||
<div className="px-1">
|
||||
<ul className="flex flex-col gap-y-0.5 overflow-y-auto list-disc">
|
||||
|
||||
let btnStringArr: string[] = []
|
||||
{sortedThreadIds.length === 0
|
||||
|
||||
const firstMsgIdx = allThreads[threadId].messages.findIndex(msg => msg.role !== 'system' && !!msg.displayContent) ?? ''
|
||||
if (firstMsgIdx !== -1)
|
||||
btnStringArr.push(truncate(allThreads[threadId].messages[firstMsgIdx].displayContent ?? ''))
|
||||
else
|
||||
btnStringArr.push('""')
|
||||
? <div key="nothreads" className="text-center text-void-fg-3 brightness-90 text-sm">{`There are no chat threads yet.`}</div>
|
||||
|
||||
const secondMsgIdx = allThreads[threadId].messages.findIndex((msg, i) => msg.role !== 'system' && !!msg.displayContent && i > firstMsgIdx) ?? ''
|
||||
if (secondMsgIdx !== -1)
|
||||
btnStringArr.push(truncate(allThreads[threadId].messages[secondMsgIdx].displayContent ?? ''))
|
||||
: sortedThreadIds.map((threadId) => {
|
||||
if (!allThreads) {
|
||||
return <li key="error" className="text-void-warning">{`Error accessing chat history.`}</li>;
|
||||
}
|
||||
|
||||
const numMessagesRemaining = allThreads[threadId].messages.filter((msg, i) => msg.role !== 'system' && !!msg.displayContent && i > secondMsgIdx).length
|
||||
if (numMessagesRemaining > 0)
|
||||
btnStringArr.push(numMessagesRemaining + '')
|
||||
const pastThread = allThreads[threadId];
|
||||
let firstMsg = null;
|
||||
// let secondMsg = null;
|
||||
|
||||
const btnString = btnStringArr.join(' / ')
|
||||
const firstMsgIdx = pastThread.messages.findIndex(
|
||||
(msg) => msg.role !== 'system' && !!msg.displayContent
|
||||
);
|
||||
|
||||
return (
|
||||
<button
|
||||
key={pastThread.id}
|
||||
className={`rounded-sm`}
|
||||
onClick={() => threadsStateService.switchToThread(pastThread.id)}
|
||||
title={new Date(pastThread.createdAt).toLocaleString()}
|
||||
>
|
||||
{btnString}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
if (firstMsgIdx !== -1) {
|
||||
// firstMsg = truncate(pastThread.messages[firstMsgIdx].displayContent ?? '');
|
||||
firstMsg = pastThread.messages[firstMsgIdx].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 !== 'system'
|
||||
).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.createdAt).toLocaleString()}
|
||||
>
|
||||
<div className='truncate'>{`${firstMsg}`}</div>
|
||||
<div>{`\u00A0(${numMessages})`}</div>
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*------------------------------------------------------------------------------------------
|
||||
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
|
||||
*-----------------------------------------------------------------------------------------*/
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* 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 { Sidebar } from './Sidebar.js'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*------------------------------------------------------------------------------------------
|
||||
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
|
||||
*-----------------------------------------------------------------------------------------*/
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@@ -20,6 +20,16 @@
|
||||
|
||||
|
||||
|
||||
.inherit-bg-all-restyle > * {
|
||||
background-color: inherit !important;
|
||||
}
|
||||
|
||||
|
||||
.bg-editor-style-override {
|
||||
--vscode-sideBar-background: var(--vscode-editor-background);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* html {
|
||||
font-size: var(--vscode-font-size);
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
|
||||
|
||||
|
||||
type ReturnType<T> = [
|
||||
{ readonly current: T },
|
||||
(t: T) => void
|
||||
]
|
||||
|
||||
// use this if state might be too slow to catch
|
||||
export const useRefState = <T,>(initVal: T): ReturnType<T> => {
|
||||
const [_, _setState] = useState(false)
|
||||
const ref = useRef<T>(initVal)
|
||||
const setState = useCallback((newVal: T) => {
|
||||
_setState(n => !n) // call rerender
|
||||
ref.current = newVal
|
||||
}, [])
|
||||
return [ref, setState]
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
/*------------------------------------------------------------------------------------------
|
||||
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
|
||||
*-----------------------------------------------------------------------------------------*/
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
import React, { forwardRef, MutableRefObject, useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';
|
||||
import { IInputBoxStyles, InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js';
|
||||
import { defaultCheckboxStyles, defaultInputBoxStyles, defaultSelectBoxStyles } from '../../../../../../../platform/theme/browser/defaultStyles.js';
|
||||
import { SelectBox } from '../../../../../../../base/browser/ui/selectBox/selectBox.js';
|
||||
@@ -12,6 +12,9 @@ import { Checkbox } from '../../../../../../../base/browser/ui/toggle/toggle.js'
|
||||
|
||||
import { CodeEditorWidget } from '../../../../../../../editor/browser/widget/codeEditor/codeEditorWidget.js'
|
||||
import { useAccessor } from './services.js';
|
||||
import { ITextModel } from '../../../../../../../editor/common/model.js';
|
||||
import { asCssVariable } from '../../../../../../../platform/theme/common/colorUtils.js';
|
||||
import { inputBackground, inputForeground } from '../../../../../../../platform/theme/common/colorRegistry.js';
|
||||
|
||||
|
||||
// type guard
|
||||
@@ -45,8 +48,105 @@ export const WidgetComponent = <CtorParams extends any[], Instance>({ ctor, prop
|
||||
}
|
||||
|
||||
|
||||
export type TextAreaFns = { setValue: (v: string) => void, enable: () => void, disable: () => void }
|
||||
type InputBox2Props = {
|
||||
initValue?: string | null;
|
||||
placeholder: string;
|
||||
multiline: boolean;
|
||||
fnsRef?: { current: null | TextAreaFns };
|
||||
className?: string;
|
||||
onChangeText?: (value: string) => void;
|
||||
onKeyDown?: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void;
|
||||
onChangeHeight?: (newHeight: number) => void;
|
||||
}
|
||||
export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(function X({ initValue, placeholder, multiline, fnsRef, className, onKeyDown, onChangeText }, ref) {
|
||||
|
||||
export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, placeholder, multiline, styles }: {
|
||||
// mirrors whatever is in ref
|
||||
const textAreaRef = useRef<HTMLTextAreaElement | null>(null)
|
||||
const [isEnabled, setEnabled] = useState(true)
|
||||
|
||||
const adjustHeight = useCallback(() => {
|
||||
const r = textAreaRef.current
|
||||
if (!r) return
|
||||
|
||||
r.style.height = 'auto' // set to auto to reset height, then set to new height
|
||||
|
||||
if (r.scrollHeight === 0) return requestAnimationFrame(adjustHeight)
|
||||
const h = r.scrollHeight
|
||||
const newHeight = Math.min(h + 1, 500) // plus one to avoid scrollbar appearing when it shouldn't
|
||||
r.style.height = `${newHeight}px`
|
||||
}, []);
|
||||
|
||||
|
||||
|
||||
const fns: TextAreaFns = useMemo(() => ({
|
||||
setValue: (val) => {
|
||||
const r = textAreaRef.current
|
||||
if (!r) return
|
||||
r.value = val
|
||||
onChangeText?.(r.value)
|
||||
adjustHeight()
|
||||
},
|
||||
enable: () => { setEnabled(true) },
|
||||
disable: () => { setEnabled(false) },
|
||||
}), [onChangeText, adjustHeight])
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (initValue)
|
||||
fns.setValue(initValue)
|
||||
}, [initValue])
|
||||
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<textarea
|
||||
ref={useCallback((r: HTMLTextAreaElement | null) => {
|
||||
if (fnsRef)
|
||||
fnsRef.current = fns
|
||||
|
||||
textAreaRef.current = r
|
||||
if (typeof ref === 'function') ref(r)
|
||||
else if (ref) ref.current = r
|
||||
adjustHeight()
|
||||
}, [fnsRef, fns, setEnabled, adjustHeight, ref])}
|
||||
|
||||
disabled={!isEnabled}
|
||||
|
||||
className={`w-full resize-none max-h-[500px] overflow-y-auto text-void-fg-1 placeholder:text-void-fg-3 ${className}`}
|
||||
style={{
|
||||
// defaultInputBoxStyles
|
||||
background: asCssVariable(inputBackground),
|
||||
color: asCssVariable(inputForeground)
|
||||
// inputBorder: asCssVariable(inputBorder),
|
||||
}}
|
||||
|
||||
onChange={useCallback(() => {
|
||||
const r = textAreaRef.current
|
||||
if (!r) return
|
||||
onChangeText?.(r.value)
|
||||
adjustHeight()
|
||||
}, [onChangeText, adjustHeight])}
|
||||
|
||||
onKeyDown={useCallback((e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
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])}
|
||||
|
||||
rows={1}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
)
|
||||
|
||||
})
|
||||
|
||||
export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, placeholder, multiline }: {
|
||||
onChangeText: (value: string) => void;
|
||||
styles?: Partial<IInputBoxStyles>,
|
||||
onCreateInstance?: (instance: InputBox) => void | IDisposable[];
|
||||
@@ -60,6 +160,10 @@ export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, plac
|
||||
const contextViewProvider = accessor.get('IContextViewService')
|
||||
return <WidgetComponent
|
||||
ctor={InputBox}
|
||||
className='
|
||||
bg-void-bg-1
|
||||
@@[&_::placeholder]:!void-text-void-fg-3
|
||||
'
|
||||
propsFn={useCallback((container) => [
|
||||
container,
|
||||
contextViewProvider,
|
||||
@@ -69,7 +173,6 @@ export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, plac
|
||||
inputForeground: "var(--vscode-foreground)",
|
||||
// inputBackground: 'transparent',
|
||||
// inputBorder: 'none',
|
||||
...styles,
|
||||
},
|
||||
placeholder,
|
||||
tooltip: '',
|
||||
@@ -193,11 +296,225 @@ export const VoidCheckBox = ({ label, value, onClick, className }: { label: stri
|
||||
}
|
||||
|
||||
|
||||
export const VoidSelectBox = <T,>({ onChangeSelection, onCreateInstance, selectBoxRef, options }: {
|
||||
export const VoidCustomSelectBox = <T extends any>({
|
||||
options,
|
||||
selectedOption: selectedOption_,
|
||||
onChangeOption,
|
||||
getOptionDropdownName,
|
||||
getOptionDisplayName,
|
||||
getOptionsEqual,
|
||||
className,
|
||||
arrowTouchesText = true,
|
||||
matchInputWidth = false,
|
||||
isMenuPositionFixed = true,
|
||||
gap = 0,
|
||||
}: {
|
||||
options: T[];
|
||||
selectedOption?: T;
|
||||
onChangeOption: (newValue: T) => void;
|
||||
getOptionDropdownName: (option: T) => string;
|
||||
getOptionDisplayName: (option: T) => string;
|
||||
getOptionsEqual: (a: T, b: T) => boolean;
|
||||
className?: string;
|
||||
arrowTouchesText?: boolean;
|
||||
matchInputWidth?: boolean;
|
||||
isMenuPositionFixed?: boolean;
|
||||
gap?: number;
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [readyToShow, setReadyToShow] = useState(false);
|
||||
const [position, setPosition] = useState({ top: 0, left: 0, width: 0 });
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const buttonRef = useRef<HTMLButtonElement | null>(null);
|
||||
const measureRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
|
||||
// if the selected option is null, use the 0th option as the selected, and set the option to options[0]
|
||||
useEffect(() => {
|
||||
if (!options[0]) return
|
||||
if (!selectedOption_) {
|
||||
onChangeOption(options[0]);
|
||||
}
|
||||
}, [selectedOption_, options])
|
||||
const selectedOption = !selectedOption_ ? options[0] : selectedOption_
|
||||
|
||||
|
||||
const updatePosition = useCallback(() => {
|
||||
if (!buttonRef.current || !containerRef.current || !measureRef.current) return;
|
||||
|
||||
const buttonRect = buttonRef.current.getBoundingClientRect();
|
||||
const containerRect = containerRef.current.getBoundingClientRect();
|
||||
const containerWidth = containerRef.current.offsetWidth;
|
||||
const viewportHeight = window.innerHeight;
|
||||
const spaceBelow = viewportHeight - buttonRect.bottom;
|
||||
const spaceNeeded = options.length * 28;
|
||||
const showAbove = spaceBelow < spaceNeeded && buttonRect.top > spaceBelow;
|
||||
|
||||
// Calculate the menu width
|
||||
let menuWidth = matchInputWidth ? containerWidth : buttonRect.width;
|
||||
|
||||
// If not matchInputWidth, calculate content width from measurement div
|
||||
if (!matchInputWidth) {
|
||||
const contentWidth = measureRef.current.offsetWidth;
|
||||
menuWidth = Math.max(buttonRect.width, contentWidth);
|
||||
}
|
||||
|
||||
if (isMenuPositionFixed) {
|
||||
// Fixed positioning (relative to viewport)
|
||||
setPosition({
|
||||
top: showAbove
|
||||
? buttonRect.top - spaceNeeded
|
||||
: buttonRect.bottom + gap,
|
||||
left: buttonRect.left,
|
||||
width: menuWidth,
|
||||
});
|
||||
} else {
|
||||
// Absolute positioning (relative to parent container)
|
||||
setPosition({
|
||||
top: showAbove
|
||||
? -(spaceNeeded + gap)
|
||||
: buttonRect.height + gap,
|
||||
left: 0,
|
||||
width: menuWidth,
|
||||
});
|
||||
}
|
||||
|
||||
setReadyToShow(true);
|
||||
}, [gap, matchInputWidth, options.length, isMenuPositionFixed]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setReadyToShow(false);
|
||||
updatePosition();
|
||||
window.addEventListener('scroll', updatePosition, true);
|
||||
window.addEventListener('resize', updatePosition);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('scroll', updatePosition, true);
|
||||
window.removeEventListener('resize', updatePosition);
|
||||
};
|
||||
} else {
|
||||
setReadyToShow(false);
|
||||
}
|
||||
}, [isOpen, updatePosition]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (isOpen) {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={`inline-block relative ${className}`}
|
||||
>
|
||||
{/* Hidden measurement div */}
|
||||
<div
|
||||
ref={measureRef}
|
||||
className="opacity-0 pointer-events-none absolute -left-[999999px] -top-[999999px] flex flex-col"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{options.map((option) => (
|
||||
<div key={getOptionDropdownName(option)} className="flex items-center whitespace-nowrap">
|
||||
<div className="w-4" />
|
||||
<span className="px-2">{getOptionDropdownName(option)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Select Button */}
|
||||
<button
|
||||
type='button'
|
||||
ref={buttonRef}
|
||||
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' : ''}`}>
|
||||
{getOptionDisplayName(selectedOption)}
|
||||
</span>
|
||||
<svg
|
||||
className={`size-3 flex-shrink-0 ${arrowTouchesText ? '' : 'ml-auto'}`}
|
||||
viewBox="0 0 12 12"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M2.5 4.5L6 8L9.5 4.5"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Dropdown Menu */}
|
||||
{isOpen && readyToShow && (
|
||||
<div
|
||||
className={`${isMenuPositionFixed ? 'fixed' : 'absolute'} z-10 bg-void-bg-1 border-void-border-1 border overflow-hidden rounded shadow-lg`}
|
||||
style={{
|
||||
top: position.top,
|
||||
left: position.left,
|
||||
width: position.width,
|
||||
}}
|
||||
>
|
||||
{options.map((option) => {
|
||||
const thisOptionIsSelected = getOptionsEqual(option, selectedOption);
|
||||
const optionName = getOptionDropdownName(option);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={optionName}
|
||||
className={`flex items-center px-2 py-1 cursor-pointer whitespace-nowrap
|
||||
transition-all duration-100
|
||||
bg-void-bg-1
|
||||
${thisOptionIsSelected ? 'bg-void-bg-2' : 'hover:bg-void-bg-2'}
|
||||
`}
|
||||
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>{optionName}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
||||
export const _VoidSelectBox = <T,>({ onChangeSelection, onCreateInstance, selectBoxRef, options, className }: {
|
||||
onChangeSelection: (value: T) => void;
|
||||
onCreateInstance?: ((instance: SelectBox) => void | IDisposable[]);
|
||||
selectBoxRef?: React.MutableRefObject<SelectBox | null>;
|
||||
options: readonly { text: string, value: T }[];
|
||||
className?: string;
|
||||
}) => {
|
||||
const accessor = useAccessor()
|
||||
const contextViewProvider = accessor.get('IContextViewService')
|
||||
@@ -205,7 +522,13 @@ export const VoidSelectBox = <T,>({ onChangeSelection, onCreateInstance, selectB
|
||||
let containerRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
return <WidgetComponent
|
||||
className='@@select-child-restyle'
|
||||
className={`
|
||||
@@select-child-restyle
|
||||
@@[&_select]:!void-text-void-fg-3
|
||||
@@[&_select]:!void-text-xs
|
||||
!text-void-fg-3
|
||||
${className ?? ''}
|
||||
`}
|
||||
ctor={SelectBox}
|
||||
propsFn={useCallback((container) => {
|
||||
containerRef.current = container
|
||||
@@ -214,14 +537,15 @@ export const VoidSelectBox = <T,>({ onChangeSelection, onCreateInstance, selectB
|
||||
options.map(opt => ({ text: opt.text })),
|
||||
defaultIndex,
|
||||
contextViewProvider,
|
||||
defaultSelectBoxStyles
|
||||
defaultSelectBoxStyles,
|
||||
] as const;
|
||||
}, [containerRef, options, contextViewProvider])}
|
||||
}, [containerRef, options])}
|
||||
|
||||
dispose={useCallback((instance: SelectBox) => {
|
||||
instance.dispose();
|
||||
for (let child of containerRef.current?.childNodes ?? [])
|
||||
containerRef.current?.childNodes.forEach(child => {
|
||||
containerRef.current?.removeChild(child)
|
||||
})
|
||||
}, [containerRef])}
|
||||
|
||||
onCreateInstance={useCallback((instance: SelectBox) => {
|
||||
@@ -286,24 +610,48 @@ const normalizeIndentation = (code: string): string => {
|
||||
|
||||
}
|
||||
|
||||
export const VoidCodeEditor = ({ initValue, language }: { initValue: string, language: string | undefined }) => {
|
||||
|
||||
const MAX_HEIGHT = Infinity;
|
||||
const modelOfEditorId: { [id: string]: ITextModel | undefined } = {}
|
||||
export type VoidCodeEditorProps = { initValue: string, language?: string, maxHeight?: number, showScrollbars?: boolean }
|
||||
export const VoidCodeEditor = ({ initValue, language, maxHeight, showScrollbars }: VoidCodeEditorProps) => {
|
||||
|
||||
initValue = normalizeIndentation(initValue)
|
||||
|
||||
// default settings
|
||||
const MAX_HEIGHT = maxHeight ?? Infinity;
|
||||
const SHOW_SCROLLBARS = showScrollbars ?? false;
|
||||
|
||||
const divRef = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
const accessor = useAccessor()
|
||||
const instantiationService = accessor.get('IInstantiationService')
|
||||
// const languageDetectionService = accessor.get('ILanguageDetectionService')
|
||||
const modelService = accessor.get('IModelService')
|
||||
const languageDetectionService = accessor.get('ILanguageDetectionService')
|
||||
|
||||
initValue = normalizeIndentation(initValue)
|
||||
|
||||
return <div ref={divRef}>
|
||||
const id = useId()
|
||||
|
||||
// these are used to pass to the model creation of modelRef
|
||||
const initValueRef = useRef(initValue)
|
||||
const languageRef = useRef(language)
|
||||
|
||||
const modelRef = useRef<ITextModel | null>(null)
|
||||
|
||||
// if we change the initial value, don't re-render the whole thing, just set it here. same for language
|
||||
useEffect(() => {
|
||||
initValueRef.current = initValue
|
||||
modelRef.current?.setValue(initValue)
|
||||
}, [initValue])
|
||||
useEffect(() => {
|
||||
languageRef.current = language
|
||||
if (language) modelRef.current?.setLanguage(language)
|
||||
}, [language])
|
||||
|
||||
return <div ref={divRef} className='relative z-0 px-2 py-1 bg-void-bg-3'>
|
||||
<WidgetComponent
|
||||
className='relative z-0 text-sm bg-vscode-editor-bg'
|
||||
ctor={useCallback((container) =>
|
||||
instantiationService.createInstance(
|
||||
className='@@bg-editor-style-override' // text-sm
|
||||
ctor={useCallback((container) => {
|
||||
return instantiationService.createInstance(
|
||||
CodeEditorWidget,
|
||||
container,
|
||||
{
|
||||
@@ -312,10 +660,19 @@ export const VoidCodeEditor = ({ initValue, language }: { initValue: string, lan
|
||||
|
||||
scrollbar: {
|
||||
alwaysConsumeMouseWheel: false,
|
||||
vertical: 'hidden',
|
||||
horizontal: 'hidden',
|
||||
verticalScrollbarSize: 0,
|
||||
horizontalScrollbarSize: 0,
|
||||
...SHOW_SCROLLBARS ? {
|
||||
vertical: 'auto',
|
||||
verticalScrollbarSize: 8,
|
||||
horizontal: 'auto',
|
||||
horizontalScrollbarSize: 8,
|
||||
} : {
|
||||
vertical: 'hidden',
|
||||
verticalScrollbarSize: 0,
|
||||
horizontal: 'auto',
|
||||
horizontalScrollbarSize: 8,
|
||||
ignoreHorizontalScrollbarInContentHeight: true,
|
||||
|
||||
},
|
||||
},
|
||||
scrollBeyondLastLine: false,
|
||||
|
||||
@@ -330,6 +687,8 @@ export const VoidCodeEditor = ({ initValue, language }: { initValue: string, lan
|
||||
// maxColumn: 0,
|
||||
},
|
||||
|
||||
hover: { enabled: false },
|
||||
|
||||
selectionHighlight: false, // highlights whole words
|
||||
renderLineHighlight: 'none',
|
||||
|
||||
@@ -347,27 +706,25 @@ export const VoidCodeEditor = ({ initValue, language }: { initValue: string, lan
|
||||
{
|
||||
isSimpleWidget: true,
|
||||
})
|
||||
, [instantiationService])
|
||||
}
|
||||
}, [instantiationService])}
|
||||
|
||||
onCreateInstance={useCallback((editor: CodeEditorWidget) => {
|
||||
const model = modelService.createModel(
|
||||
initValue,
|
||||
language ? {
|
||||
languageId: language,
|
||||
onDidChange: () => ({
|
||||
dispose: () => { }
|
||||
})
|
||||
} : null
|
||||
);
|
||||
const model = modelOfEditorId[id] ?? modelService.createModel(
|
||||
initValueRef.current, {
|
||||
languageId: languageRef.current ? languageRef.current : 'typescript',
|
||||
onDidChange: (e) => { return { dispose: () => { } } } // no idea why they'd require this
|
||||
})
|
||||
modelRef.current = model
|
||||
editor.setModel(model);
|
||||
|
||||
const container = editor.getDomNode()
|
||||
const parentNode = container?.parentElement
|
||||
const resize = () => {
|
||||
const height = editor.getScrollHeight() + 1
|
||||
if (parentNode) {
|
||||
const height = Math.min(editor.getScrollHeight() + 1, MAX_HEIGHT);
|
||||
// const height = Math.min(, MAX_HEIGHT);
|
||||
parentNode.style.height = `${height}px`;
|
||||
parentNode.style.maxHeight = `${MAX_HEIGHT}px`;
|
||||
editor.layout();
|
||||
}
|
||||
}
|
||||
@@ -375,12 +732,12 @@ export const VoidCodeEditor = ({ initValue, language }: { initValue: string, lan
|
||||
resize()
|
||||
const disposable = editor.onDidContentSizeChange(() => { resize() });
|
||||
|
||||
return [disposable]
|
||||
}, [modelService, initValue, language])}
|
||||
return [disposable, model]
|
||||
}, [modelService])}
|
||||
|
||||
dispose={useCallback((editor: CodeEditorWidget) => {
|
||||
editor.dispose();
|
||||
}, [modelService, languageDetectionService])}
|
||||
}, [modelService])}
|
||||
|
||||
propsFn={useCallback(() => { return [] }, [])}
|
||||
/>
|
||||
@@ -389,6 +746,13 @@ export const VoidCodeEditor = ({ initValue, language }: { initValue: string, lan
|
||||
}
|
||||
|
||||
|
||||
export const VoidButton = ({ children, disabled, onClick }: { children: React.ReactNode; disabled?: boolean; onClick: () => void }) => {
|
||||
return <button disabled={disabled}
|
||||
className='px-3 py-1 bg-black/10 dark:bg-gray-200/10 rounded-sm overflow-hidden'
|
||||
onClick={onClick}
|
||||
>{children}</button>
|
||||
}
|
||||
|
||||
// export const VoidScrollableElt = ({ options, children }: { options: ScrollableElementCreationOptions, children: React.ReactNode }) => {
|
||||
// const instanceRef = useRef<DomScrollableElement | null>(null);
|
||||
// const [childrenPortal, setChildrenPortal] = useState<React.ReactNode | null>(null)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*------------------------------------------------------------------------------------------
|
||||
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
|
||||
*-----------------------------------------------------------------------------------------*/
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import * as ReactDOM from 'react-dom/client'
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
/*------------------------------------------------------------------------------------------
|
||||
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
|
||||
*-----------------------------------------------------------------------------------------*/
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { ThreadsState } from '../../../threadHistoryService.js'
|
||||
import { ThreadStreamState, ThreadsState } from '../../../chatThreadService.js'
|
||||
import { RefreshableProviderName, SettingsOfProvider } from '../../../../../../../platform/void/common/voidSettingsTypes.js'
|
||||
import { IDisposable } from '../../../../../../../base/common/lifecycle.js'
|
||||
import { VoidSidebarState } from '../../../sidebarStateService.js'
|
||||
import { VoidSettingsState } from '../../../../../../../platform/void/common/voidSettingsService.js'
|
||||
import { ColorScheme } from '../../../../../../../platform/theme/common/theme.js'
|
||||
import { VoidUriState } from '../../../voidUriStateService.js';
|
||||
import { VoidQuickEditState } from '../../../quickEditStateService.js'
|
||||
import { RefreshModelStateOfProvider } from '../../../../../../../platform/void/common/refreshModelService.js'
|
||||
|
||||
@@ -28,9 +29,10 @@ import { ILLMMessageService } from '../../../../../../../platform/void/common/ll
|
||||
import { IRefreshModelService } from '../../../../../../../platform/void/common/refreshModelService.js';
|
||||
import { IVoidSettingsService } from '../../../../../../../platform/void/common/voidSettingsService.js';
|
||||
import { IInlineDiffsService } from '../../../inlineDiffsService.js';
|
||||
import { IVoidUriStateService } from '../../../voidUriStateService.js';
|
||||
import { IQuickEditStateService } from '../../../quickEditStateService.js';
|
||||
import { ISidebarStateService } from '../../../sidebarStateService.js';
|
||||
import { IThreadHistoryService } from '../../../threadHistoryService.js';
|
||||
import { IChatThreadService } from '../../../chatThreadService.js';
|
||||
import { IInstantiationService } from '../../../../../../../platform/instantiation/common/instantiation.js'
|
||||
import { ICodeEditorService } from '../../../../../../../editor/browser/services/codeEditorService.js'
|
||||
import { ICommandService } from '../../../../../../../platform/commands/common/commands.js'
|
||||
@@ -40,6 +42,11 @@ import { IAccessibilityService } from '../../../../../../../platform/accessibili
|
||||
import { ILanguageConfigurationService } from '../../../../../../../editor/common/languages/languageConfigurationRegistry.js'
|
||||
import { ILanguageFeaturesService } from '../../../../../../../editor/common/services/languageFeatures.js'
|
||||
import { ILanguageDetectionService } from '../../../../../../services/languageDetection/common/languageDetectionWorkerService.js'
|
||||
import { IKeybindingService } from '../../../../../../../platform/keybinding/common/keybinding.js'
|
||||
import { IEnvironmentService } from '../../../../../../../platform/environment/common/environment.js'
|
||||
import { IConfigurationService } from '../../../../../../../platform/configuration/common/configuration.js'
|
||||
import { IPathService } from '../../../../../../../workbench/services/path/common/pathService.js'
|
||||
import { IMetricsService } from '../../../../../../../platform/void/common/metricsService.js'
|
||||
|
||||
|
||||
|
||||
@@ -47,14 +54,20 @@ import { ILanguageDetectionService } from '../../../../../../services/languageDe
|
||||
|
||||
// even if React hasn't mounted yet, the variables are always updated to the latest state.
|
||||
// React listens by adding a setState function to these listeners.
|
||||
let uriState: VoidUriState
|
||||
const uriStateListeners: Set<(s: VoidUriState) => void> = new Set()
|
||||
|
||||
let quickEditState: VoidQuickEditState
|
||||
const quickEditStateListeners: Set<(s: VoidQuickEditState) => void> = new Set()
|
||||
|
||||
let sidebarState: VoidSidebarState
|
||||
const sidebarStateListeners: Set<(s: VoidSidebarState) => void> = new Set()
|
||||
|
||||
let threadsState: ThreadsState
|
||||
const threadsStateListeners: Set<(s: ThreadsState) => void> = new Set()
|
||||
let chatThreadsState: ThreadsState
|
||||
const chatThreadsStateListeners: Set<(s: ThreadsState) => void> = new Set()
|
||||
|
||||
let chatThreadsStreamState: ThreadStreamState
|
||||
const chatThreadsStreamStateListeners: Set<(threadId: string) => void> = new Set()
|
||||
|
||||
let settingsState: VoidSettingsState
|
||||
const settingsStateListeners: Set<(s: VoidSettingsState) => void> = new Set()
|
||||
@@ -83,15 +96,25 @@ export const _registerServices = (accessor: ServicesAccessor) => {
|
||||
_registerAccessor(accessor)
|
||||
|
||||
const stateServices = {
|
||||
uriStateService: accessor.get(IVoidUriStateService),
|
||||
quickEditStateService: accessor.get(IQuickEditStateService),
|
||||
sidebarStateService: accessor.get(ISidebarStateService),
|
||||
threadsStateService: accessor.get(IThreadHistoryService),
|
||||
chatThreadsStateService: accessor.get(IChatThreadService),
|
||||
settingsStateService: accessor.get(IVoidSettingsService),
|
||||
refreshModelService: accessor.get(IRefreshModelService),
|
||||
themeService: accessor.get(IThemeService),
|
||||
inlineDiffsService: accessor.get(IInlineDiffsService),
|
||||
}
|
||||
|
||||
const { sidebarStateService, quickEditStateService, settingsStateService, threadsStateService, refreshModelService, themeService, } = stateServices
|
||||
const { uriStateService, sidebarStateService, quickEditStateService, settingsStateService, chatThreadsStateService, refreshModelService, themeService, inlineDiffsService } = stateServices
|
||||
|
||||
uriState = uriStateService.state
|
||||
disposables.push(
|
||||
uriStateService.onDidChangeState(() => {
|
||||
uriState = uriStateService.state
|
||||
uriStateListeners.forEach(l => l(uriState))
|
||||
})
|
||||
)
|
||||
|
||||
quickEditState = quickEditStateService.state
|
||||
disposables.push(
|
||||
@@ -109,11 +132,20 @@ export const _registerServices = (accessor: ServicesAccessor) => {
|
||||
})
|
||||
)
|
||||
|
||||
threadsState = threadsStateService.state
|
||||
chatThreadsState = chatThreadsStateService.state
|
||||
disposables.push(
|
||||
threadsStateService.onDidChangeCurrentThread(() => {
|
||||
threadsState = threadsStateService.state
|
||||
threadsStateListeners.forEach(l => l(threadsState))
|
||||
chatThreadsStateService.onDidChangeCurrentThread(() => {
|
||||
chatThreadsState = chatThreadsStateService.state
|
||||
chatThreadsStateListeners.forEach(l => l(chatThreadsState))
|
||||
})
|
||||
)
|
||||
|
||||
// same service, different state
|
||||
chatThreadsStreamState = chatThreadsStateService.streamState
|
||||
disposables.push(
|
||||
chatThreadsStateService.onDidChangeStreamState(({ threadId }) => {
|
||||
chatThreadsStreamState = chatThreadsStateService.streamState
|
||||
chatThreadsStreamStateListeners.forEach(l => l(threadId))
|
||||
})
|
||||
)
|
||||
|
||||
@@ -142,6 +174,7 @@ export const _registerServices = (accessor: ServicesAccessor) => {
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
return disposables
|
||||
}
|
||||
|
||||
@@ -160,9 +193,10 @@ const getReactAccessor = (accessor: ServicesAccessor) => {
|
||||
IRefreshModelService: accessor.get(IRefreshModelService),
|
||||
IVoidSettingsService: accessor.get(IVoidSettingsService),
|
||||
IInlineDiffsService: accessor.get(IInlineDiffsService),
|
||||
IVoidUriStateService: accessor.get(IVoidUriStateService),
|
||||
IQuickEditStateService: accessor.get(IQuickEditStateService),
|
||||
ISidebarStateService: accessor.get(ISidebarStateService),
|
||||
IThreadHistoryService: accessor.get(IThreadHistoryService),
|
||||
IChatThreadService: accessor.get(IChatThreadService),
|
||||
|
||||
IInstantiationService: accessor.get(IInstantiationService),
|
||||
ICodeEditorService: accessor.get(ICodeEditorService),
|
||||
@@ -173,6 +207,12 @@ const getReactAccessor = (accessor: ServicesAccessor) => {
|
||||
ILanguageConfigurationService: accessor.get(ILanguageConfigurationService),
|
||||
ILanguageDetectionService: accessor.get(ILanguageDetectionService),
|
||||
ILanguageFeaturesService: accessor.get(ILanguageFeaturesService),
|
||||
IKeybindingService: accessor.get(IKeybindingService),
|
||||
|
||||
IEnvironmentService: accessor.get(IEnvironmentService),
|
||||
IConfigurationService: accessor.get(IConfigurationService),
|
||||
IPathService: accessor.get(IPathService),
|
||||
IMetricsService: accessor.get(IMetricsService),
|
||||
|
||||
} as const
|
||||
return reactAccessor
|
||||
@@ -200,6 +240,16 @@ export const useAccessor = () => {
|
||||
|
||||
// -- state of services --
|
||||
|
||||
export const useUriState = () => {
|
||||
const [s, ss] = useState(uriState)
|
||||
useEffect(() => {
|
||||
ss(uriState)
|
||||
uriStateListeners.add(ss)
|
||||
return () => { uriStateListeners.delete(ss) }
|
||||
}, [ss])
|
||||
return s
|
||||
}
|
||||
|
||||
export const useQuickEditState = () => {
|
||||
const [s, ss] = useState(quickEditState)
|
||||
useEffect(() => {
|
||||
@@ -230,17 +280,36 @@ export const useSettingsState = () => {
|
||||
return s
|
||||
}
|
||||
|
||||
export const useThreadsState = () => {
|
||||
const [s, ss] = useState(threadsState)
|
||||
export const useChatThreadsState = () => {
|
||||
const [s, ss] = useState(chatThreadsState)
|
||||
useEffect(() => {
|
||||
ss(threadsState)
|
||||
threadsStateListeners.add(ss)
|
||||
return () => { threadsStateListeners.delete(ss) }
|
||||
ss(chatThreadsState)
|
||||
chatThreadsStateListeners.add(ss)
|
||||
return () => { chatThreadsStateListeners.delete(ss) }
|
||||
}, [ss])
|
||||
return s
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
export const useChatThreadsStreamState = (threadId: string) => {
|
||||
const [s, ss] = useState<ThreadStreamState[string] | undefined>(chatThreadsStreamState[threadId])
|
||||
useEffect(() => {
|
||||
ss(chatThreadsStreamState[threadId])
|
||||
const listener = (threadId_: string) => {
|
||||
if (threadId_ !== threadId) return
|
||||
ss(chatThreadsStreamState[threadId])
|
||||
}
|
||||
chatThreadsStreamStateListeners.add(listener)
|
||||
return () => { chatThreadsStreamStateListeners.delete(listener) }
|
||||
}, [ss, threadId])
|
||||
return s
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
export const useRefreshModelState = () => {
|
||||
const [s, ss] = useState(refreshModelState)
|
||||
useEffect(() => {
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export const useScrollbarStyles = (containerRef: React.MutableRefObject<HTMLDivElement | null>) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
// Create selector for specific overflow classes
|
||||
const overflowSelector = [
|
||||
'[class*="overflow-auto"]',
|
||||
'[class*="overflow-x-auto"]',
|
||||
'[class*="overflow-y-auto"]'
|
||||
].join(',');
|
||||
|
||||
// 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))
|
||||
];
|
||||
|
||||
// Apply styles and listeners to each scroll element
|
||||
scrollElements.forEach(element => {
|
||||
// Add the scrollable class directly to the overflow element
|
||||
element.classList.add('void-scrollable-element');
|
||||
|
||||
let fadeTimeout: NodeJS.Timeout | null = null;
|
||||
let fadeInterval: NodeJS.Timeout | null = null;
|
||||
|
||||
const fadeIn = () => {
|
||||
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);
|
||||
};
|
||||
|
||||
const fadeOut = () => {
|
||||
if (fadeInterval) clearInterval(fadeInterval);
|
||||
|
||||
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 onMouseEnter = () => {
|
||||
if (fadeTimeout) clearTimeout(fadeTimeout);
|
||||
if (fadeInterval) clearInterval(fadeInterval);
|
||||
fadeIn();
|
||||
};
|
||||
|
||||
const onMouseLeave = () => {
|
||||
if (fadeTimeout) clearTimeout(fadeTimeout);
|
||||
fadeTimeout = setTimeout(() => {
|
||||
fadeOut();
|
||||
}, 10);
|
||||
};
|
||||
|
||||
element.addEventListener('mouseenter', onMouseEnter);
|
||||
element.addEventListener('mouseleave', onMouseLeave);
|
||||
|
||||
// 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}`);
|
||||
}
|
||||
};
|
||||
|
||||
// Store the cleanup function on the element for later use
|
||||
(element as any).__scrollbarCleanup = cleanup;
|
||||
});
|
||||
|
||||
return () => {
|
||||
// Clean up all scroll elements
|
||||
scrollElements.forEach(element => {
|
||||
if ((element as any).__scrollbarCleanup) {
|
||||
(element as any).__scrollbarCleanup();
|
||||
}
|
||||
});
|
||||
};
|
||||
}, [containerRef]);
|
||||
};
|
||||
+72
-48
@@ -1,15 +1,15 @@
|
||||
/*------------------------------------------------------------------------------------------
|
||||
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
|
||||
*-----------------------------------------------------------------------------------------*/
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { FeatureName, featureNames, ModelSelection, modelSelectionsEqual, ProviderName, providerNames } from '../../../../../../../platform/void/common/voidSettingsTypes.js'
|
||||
import { useSettingsState, useRefreshModelState, useAccessor } from '../util/services.js'
|
||||
import { VoidSelectBox } from '../util/inputs.js'
|
||||
import { _VoidSelectBox, VoidCustomSelectBox } from '../util/inputs.js'
|
||||
import { SelectBox } from '../../../../../../../base/browser/ui/selectBox/selectBox.js'
|
||||
import { IconWarning } from '../sidebar-tsx/SidebarChat.js'
|
||||
import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js'
|
||||
import { VOID_OPEN_SETTINGS_ACTION_ID, VOID_TOGGLE_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js'
|
||||
import { ModelOption } from '../../../../../../../platform/void/common/voidSettingsService.js'
|
||||
|
||||
|
||||
@@ -17,42 +17,64 @@ import { ModelOption } from '../../../../../../../platform/void/common/voidSetti
|
||||
const optionsEqual = (m1: ModelOption[], m2: ModelOption[]) => {
|
||||
if (m1.length !== m2.length) return false
|
||||
for (let i = 0; i < m1.length; i++) {
|
||||
if (!modelSelectionsEqual(m1[i].value, m2[i].value)) return false
|
||||
if (!modelSelectionsEqual(m1[i].selection, m2[i].selection)) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
|
||||
const ModelSelectBox = ({ options, featureName }: { options: ModelOption[], featureName: FeatureName }) => {
|
||||
const accessor = useAccessor()
|
||||
|
||||
const voidSettingsService = accessor.get('IVoidSettingsService')
|
||||
|
||||
let weChangedText = false
|
||||
const selection = voidSettingsService.state.modelSelectionOfFeature[featureName]
|
||||
const selectedOption = selection ? voidSettingsService.state._modelOptions.find(v => modelSelectionsEqual(v.selection, selection)) : options[0]
|
||||
|
||||
return <VoidSelectBox
|
||||
const onChangeOption = useCallback((newOption: ModelOption) => {
|
||||
voidSettingsService.setModelSelectionOfFeature(featureName, newOption.selection)
|
||||
}, [voidSettingsService, featureName])
|
||||
|
||||
return <VoidCustomSelectBox
|
||||
options={options}
|
||||
onChangeSelection={useCallback((newVal: ModelSelection) => {
|
||||
if (weChangedText) return
|
||||
voidSettingsService.setModelSelectionOfFeature(featureName, newVal)
|
||||
}, [voidSettingsService, featureName])}
|
||||
// we are responsible for setting the initial state here. always sync instance when state changes.
|
||||
onCreateInstance={useCallback((instance: SelectBox) => {
|
||||
const syncInstance = () => {
|
||||
const modelsListRef = voidSettingsService.state._modelOptions // as a ref
|
||||
const settingsAtProvider = voidSettingsService.state.modelSelectionOfFeature[featureName]
|
||||
const selectionIdx = settingsAtProvider === null ? -1 : modelsListRef.findIndex(v => modelSelectionsEqual(v.value, settingsAtProvider))
|
||||
weChangedText = true
|
||||
instance.select(selectionIdx === -1 ? 0 : selectionIdx)
|
||||
weChangedText = false
|
||||
}
|
||||
syncInstance()
|
||||
const disposable = voidSettingsService.onDidChangeState(syncInstance)
|
||||
return [disposable]
|
||||
}, [voidSettingsService, featureName])}
|
||||
selectedOption={selectedOption}
|
||||
onChangeOption={onChangeOption}
|
||||
getOptionDisplayName={(option) => option.selection.modelName}
|
||||
getOptionDropdownName={(option) => option.name}
|
||||
getOptionsEqual={(a, b) => optionsEqual([a], [b])}
|
||||
className={`text-xs text-void-fg-3 px-1`}
|
||||
matchInputWidth={false}
|
||||
isMenuPositionFixed={featureName === 'Ctrl+K' ? false : true}
|
||||
/>
|
||||
}
|
||||
// const ModelSelectBox = ({ options, featureName }: { options: ModelOption[], featureName: FeatureName }) => {
|
||||
// const accessor = useAccessor()
|
||||
|
||||
// const voidSettingsService = accessor.get('IVoidSettingsService')
|
||||
|
||||
// let weChangedText = false
|
||||
|
||||
// return <VoidSelectBox
|
||||
// className='@@[&_select]:!void-text-xs text-void-fg-3'
|
||||
// options={options}
|
||||
// onChangeSelection={useCallback((newVal: ModelSelection) => {
|
||||
// if (weChangedText) return
|
||||
// voidSettingsService.setModelSelectionOfFeature(featureName, newVal)
|
||||
// }, [voidSettingsService, featureName])}
|
||||
// // we are responsible for setting the initial state here. always sync instance when state changes.
|
||||
// onCreateInstance={useCallback((instance: SelectBox) => {
|
||||
// const syncInstance = () => {
|
||||
// const modelsListRef = voidSettingsService.state._modelOptions // as a ref
|
||||
// const settingsAtProvider = voidSettingsService.state.modelSelectionOfFeature[featureName]
|
||||
// const selectionIdx = settingsAtProvider === null ? -1 : modelsListRef.findIndex(v => modelSelectionsEqual(v.value, settingsAtProvider))
|
||||
// weChangedText = true
|
||||
// instance.select(selectionIdx === -1 ? 0 : selectionIdx)
|
||||
// weChangedText = false
|
||||
// }
|
||||
// syncInstance()
|
||||
// const disposable = voidSettingsService.onDidChangeState(syncInstance)
|
||||
// return [disposable]
|
||||
// }, [voidSettingsService, featureName])}
|
||||
// />
|
||||
// }
|
||||
|
||||
const MemoizedModelSelectBox = ({ featureName }: { featureName: FeatureName }) => {
|
||||
const settingsState = useSettingsState()
|
||||
@@ -71,30 +93,23 @@ const MemoizedModelSelectBox = ({ featureName }: { featureName: FeatureName }) =
|
||||
|
||||
}
|
||||
|
||||
const DummySelectBox = () => {
|
||||
|
||||
const accessor = useAccessor()
|
||||
const comandService = accessor.get('ICommandService')
|
||||
|
||||
const openSettings = () => {
|
||||
comandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID);
|
||||
};
|
||||
export const WarningBox = ({ text, onClick, className }: { text: string; onClick?: () => void; className?: string }) => {
|
||||
|
||||
return <div
|
||||
className={`
|
||||
flex items-center
|
||||
flex-nowrap text-ellipsis
|
||||
text-vscode-charts-yellow
|
||||
hover:brightness-90 transition-all duration-200
|
||||
cursor-pointer
|
||||
text-void-warning brightness-90 opacity-90
|
||||
text-xs text-ellipsis
|
||||
${onClick ? `hover:brightness-75 transition-all duration-200 cursor-pointer` : ''}
|
||||
flex items-center flex-nowrap
|
||||
${className}
|
||||
`}
|
||||
onClick={openSettings}
|
||||
onClick={onClick}
|
||||
>
|
||||
<IconWarning
|
||||
size={20}
|
||||
className='mr-1 brightness-90'
|
||||
size={14}
|
||||
className='mr-1'
|
||||
/>
|
||||
<span>Model required</span>
|
||||
<span>{text}</span>
|
||||
</div>
|
||||
// return <VoidSelectBox
|
||||
// options={[{ text: 'Please add a model!', value: null }]}
|
||||
@@ -104,7 +119,16 @@ const DummySelectBox = () => {
|
||||
|
||||
export const ModelDropdown = ({ featureName }: { featureName: FeatureName }) => {
|
||||
const settingsState = useSettingsState()
|
||||
|
||||
const accessor = useAccessor()
|
||||
const commandService = accessor.get('ICommandService')
|
||||
|
||||
const openSettings = () => { commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID); };
|
||||
|
||||
return <>
|
||||
{settingsState._modelOptions.length === 0 ? <DummySelectBox /> : <MemoizedModelSelectBox featureName={featureName} />}
|
||||
{settingsState._modelOptions.length === 0 ?
|
||||
<WarningBox onClick={openSettings} text='Provider required' />
|
||||
: <MemoizedModelSelectBox featureName={featureName} />
|
||||
}
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -1,24 +1,29 @@
|
||||
/*------------------------------------------------------------------------------------------
|
||||
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
|
||||
*-----------------------------------------------------------------------------------------*/
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js'
|
||||
import { ProviderName, SettingName, displayInfoOfSettingName, providerNames, VoidModelInfo, featureFlagNames, displayInfoOfFeatureFlag, customSettingNamesOfProvider, RefreshableProviderName, refreshableProviderNames, displayInfoOfProviderName, defaultProviderSettings, nonlocalProviderNames, localProviderNames } from '../../../../../../../platform/void/common/voidSettingsTypes.js'
|
||||
import { ProviderName, SettingName, displayInfoOfSettingName, providerNames, VoidModelInfo, globalSettingNames, customSettingNamesOfProvider, RefreshableProviderName, refreshableProviderNames, displayInfoOfProviderName, defaultProviderSettings, nonlocalProviderNames, localProviderNames, GlobalSettingName } from '../../../../../../../platform/void/common/voidSettingsTypes.js'
|
||||
import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js'
|
||||
import { VoidCheckBox, VoidInputBox, VoidSelectBox, VoidSwitch } from '../util/inputs.js'
|
||||
import { VoidButton, VoidCheckBox, VoidCustomSelectBox, VoidInputBox, VoidInputBox2, VoidSwitch } from '../util/inputs.js'
|
||||
import { useAccessor, useIsDark, useRefreshModelListener, useRefreshModelState, useSettingsState } from '../util/services.js'
|
||||
import { X, RefreshCw, Loader2, Check } from 'lucide-react'
|
||||
import { X, RefreshCw, Loader2, Check, MoveRight } from 'lucide-react'
|
||||
import { useScrollbarStyles } from '../util/useScrollbarStyles.js'
|
||||
import { isWindows, isLinux, isMacintosh } from '../../../../../../../base/common/platform.js'
|
||||
import { URI } from '../../../../../../../base/common/uri.js'
|
||||
import { env } from '../../../../../../../base/common/process.js'
|
||||
import { WarningBox } from './ModelDropdown.js'
|
||||
import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js'
|
||||
|
||||
const SubtleButton = ({ onClick, text, icon, disabled }: { onClick: () => void, text: string, icon: React.ReactNode, disabled: boolean }) => {
|
||||
|
||||
return <div className='flex items-center px-3 rounded-sm overflow-hidden gap-2 hover:bg-black/10 dark:hover:bg-gray-300/10'>
|
||||
return <div className='flex items-center text-void-fg-3 mb-1 px-3 rounded-sm overflow-hidden gap-2 hover:bg-black/10 dark:hover:bg-gray-300/10'>
|
||||
<button className='flex items-center' disabled={disabled} onClick={onClick}>
|
||||
{icon}
|
||||
</button>
|
||||
<span className='opacity-50'>
|
||||
<span>
|
||||
{text}
|
||||
</span>
|
||||
</div>
|
||||
@@ -31,30 +36,41 @@ const RefreshModelButton = ({ providerName }: { providerName: RefreshableProvide
|
||||
|
||||
const accessor = useAccessor()
|
||||
const refreshModelService = accessor.get('IRefreshModelService')
|
||||
const metricsService = accessor.get('IMetricsService')
|
||||
|
||||
const [justFinished, setJustFinished] = useState(false)
|
||||
const [justFinished, setJustFinished] = useState<null | 'finished' | 'error'>(null)
|
||||
|
||||
useRefreshModelListener(
|
||||
useCallback((providerName2, refreshModelState) => {
|
||||
if (providerName2 !== providerName) return
|
||||
const { state } = refreshModelState[providerName]
|
||||
if (state !== 'finished') return
|
||||
if (!(state === 'finished' || state === 'error')) return
|
||||
// now we know we just entered 'finished' state for this providerName
|
||||
setJustFinished(true)
|
||||
const tid = setTimeout(() => { setJustFinished(false) }, 2000)
|
||||
setJustFinished(state)
|
||||
const tid = setTimeout(() => { setJustFinished(null) }, 2000)
|
||||
return () => clearTimeout(tid)
|
||||
}, [providerName])
|
||||
)
|
||||
|
||||
const { state } = refreshModelState[providerName]
|
||||
const isRefreshing = state === 'refreshing'
|
||||
|
||||
const { title: providerTitle } = displayInfoOfProviderName(providerName)
|
||||
return <SubtleButton
|
||||
onClick={() => { refreshModelService.refreshModels(providerName) }}
|
||||
text={justFinished ? `${providerTitle} Models are up-to-date!` : `Refresh Models List for ${providerTitle}.`}
|
||||
icon={isRefreshing ? <Loader2 className='size-3 animate-spin' /> : (justFinished ? <Check className='stroke-green-500 size-3' /> : <RefreshCw className='size-3' />)}
|
||||
disabled={isRefreshing || justFinished}
|
||||
onClick={() => {
|
||||
refreshModelService.startRefreshingModels(providerName, { enableProviderOnSuccess: false, doNotFire: false })
|
||||
metricsService.capture('Click', { providerName, action: 'Refresh Models' })
|
||||
}}
|
||||
text={justFinished === 'finished' ? `${providerTitle} Models are up-to-date!`
|
||||
: justFinished === 'error' ? `${providerTitle} not found!`
|
||||
: `Manually refresh ${providerTitle} models.`
|
||||
}
|
||||
icon={justFinished === 'finished' ? <Check className='stroke-green-500 size-3' />
|
||||
: justFinished === 'error' ? <X className='stroke-red-500 size-3' />
|
||||
: state === 'refreshing' ? <Loader2 className='size-3 animate-spin' />
|
||||
: <RefreshCw className='size-3' />
|
||||
}
|
||||
|
||||
disabled={state === 'refreshing' || justFinished !== null}
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -64,7 +80,7 @@ const RefreshableModels = () => {
|
||||
|
||||
const buttons = refreshableProviderNames.map(providerName => {
|
||||
if (!settingsState.settingsOfProvider[providerName]._enabled) return null
|
||||
return <div key={providerName} className='pb-4' >
|
||||
return <div key={providerName} className='pb-4'>
|
||||
<RefreshModelButton providerName={providerName} />
|
||||
</div>
|
||||
})
|
||||
@@ -84,68 +100,75 @@ const AddModelMenu = ({ onSubmit }: { onSubmit: () => void }) => {
|
||||
|
||||
const settingsState = useSettingsState()
|
||||
|
||||
const providerNameRef = useRef<ProviderName | null>(null)
|
||||
const modelNameRef = useRef<string | null>(null)
|
||||
// const providerNameRef = useRef<ProviderName | null>(null)
|
||||
const [providerName, setProviderName] = useState<ProviderName | null>(null)
|
||||
|
||||
const modelNameRef = useRef<HTMLTextAreaElement | null>(null)
|
||||
|
||||
const [errorString, setErrorString] = useState('')
|
||||
|
||||
|
||||
const providerOptions = useMemo(() => providerNames.map(providerName => ({ text: displayInfoOfProviderName(providerName).title, value: providerName })), [providerNames])
|
||||
|
||||
return <>
|
||||
<div className='flex items-center gap-4'>
|
||||
|
||||
{/* provider */}
|
||||
<div className='max-w-40 w-full border border-vscode-editorwidget-border'>
|
||||
<VoidSelectBox
|
||||
<VoidCustomSelectBox
|
||||
options={providerNames}
|
||||
selectedOption={providerName}
|
||||
onChangeOption={(pn) => setProviderName(pn)}
|
||||
getOptionDisplayName={(pn) => pn ? displayInfoOfProviderName(pn).title : '(null)'}
|
||||
getOptionDropdownName={(pn) => pn ? displayInfoOfProviderName(pn).title : '(null)'}
|
||||
getOptionsEqual={(a, b) => a === b}
|
||||
className={`max-w-44 w-full border border-void-border-2 bg-void-bg-1 text-void-fg-3 text-root
|
||||
py-[4px] px-[6px]
|
||||
`}
|
||||
arrowTouchesText={false}
|
||||
/>
|
||||
{/* <_VoidSelectBox
|
||||
onCreateInstance={useCallback(() => { providerNameRef.current = providerOptions[0].value }, [providerOptions])} // initialize state
|
||||
onChangeSelection={useCallback((providerName: ProviderName) => { providerNameRef.current = providerName }, [])}
|
||||
options={providerOptions}
|
||||
/>
|
||||
</div>
|
||||
/> */}
|
||||
|
||||
{/* model */}
|
||||
<div className='max-w-40 w-full border border-vscode-editorwidget-border'>
|
||||
<VoidInputBox
|
||||
<div className='max-w-44 w-full border border-void-border-2 bg-void-bg-1 text-void-fg-3 text-root'>
|
||||
<VoidInputBox2
|
||||
placeholder='Model Name'
|
||||
onChangeText={useCallback((modelName) => { modelNameRef.current = modelName }, [])}
|
||||
className='mt-[2px] px-[6px] h-full w-full'
|
||||
ref={modelNameRef}
|
||||
multiline={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* button */}
|
||||
<div className='max-w-40'>
|
||||
<button
|
||||
className='px-3 py-1 bg-black/10 dark:bg-gray-200/10 rounded-sm overflow-hidden'
|
||||
onClick={() => {
|
||||
const providerName = providerNameRef.current
|
||||
const modelName = modelNameRef.current
|
||||
<VoidButton onClick={() => {
|
||||
const modelName = modelNameRef.current?.value
|
||||
|
||||
if (providerName === null) {
|
||||
setErrorString('Please select a provider.')
|
||||
return
|
||||
}
|
||||
if (!modelName) {
|
||||
setErrorString('Please enter a model name.')
|
||||
return
|
||||
}
|
||||
// if model already exists here
|
||||
if (settingsState.settingsOfProvider[providerName].models.find(m => m.modelName === modelName)) {
|
||||
setErrorString(`This model already exists under ${providerName}.`)
|
||||
return
|
||||
}
|
||||
if (providerName === null) {
|
||||
setErrorString('Please select a provider.')
|
||||
return
|
||||
}
|
||||
if (!modelName) {
|
||||
setErrorString('Please enter a model name.')
|
||||
return
|
||||
}
|
||||
// if model already exists here
|
||||
if (settingsState.settingsOfProvider[providerName].models.find(m => m.modelName === modelName)) {
|
||||
setErrorString(`This model already exists under ${providerName}.`)
|
||||
return
|
||||
}
|
||||
|
||||
settingsStateService.addModel(providerName, modelName)
|
||||
onSubmit()
|
||||
settingsStateService.addModel(providerName, modelName)
|
||||
onSubmit()
|
||||
|
||||
}}>Add model</button>
|
||||
}}
|
||||
>Add model</VoidButton>
|
||||
</div>
|
||||
|
||||
{!errorString ? null : <div className='text-red-500 truncate whitespace-nowrap'>
|
||||
{errorString}
|
||||
</div>}
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</>
|
||||
@@ -158,10 +181,7 @@ const AddModelMenuFull = () => {
|
||||
return <div className='hover:bg-black/10 dark:hover:bg-gray-300/10 py-1 my-4 pb-1 px-3 rounded-sm overflow-hidden '>
|
||||
{open ?
|
||||
<AddModelMenu onSubmit={() => { setOpen(false) }} />
|
||||
: <button
|
||||
className='px-3 py-1 bg-black/10 dark:bg-gray-200/10 rounded-sm overflow-hidden'
|
||||
onClick={() => setOpen(true)}
|
||||
>Add Model</button>
|
||||
: <VoidButton onClick={() => setOpen(true)}>Add Model</VoidButton>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@@ -195,20 +215,25 @@ export const ModelDump = () => {
|
||||
|
||||
const disabled = !providerEnabled
|
||||
|
||||
return <div key={`${modelName}${providerName}`} className={`flex items-center justify-between gap-4 hover:bg-black/10 dark:hover:bg-gray-300/10 py-1 px-3 rounded-sm overflow-hidden cursor-default truncate ${isNewProviderName ? 'mt-4' : ''}`}>
|
||||
return <div key={`${modelName}${providerName}`}
|
||||
className={`flex items-center justify-between gap-4 hover:bg-black/10 dark:hover:bg-gray-300/10 py-1 px-3 rounded-sm overflow-hidden cursor-default truncate
|
||||
`}
|
||||
>
|
||||
{/* left part is width:full */}
|
||||
<div className={`w-full flex items-center gap-4`}>
|
||||
<span className='min-w-40'>{isNewProviderName ? displayInfoOfProviderName(providerName).title : ''}</span>
|
||||
<span>{modelName}</span>
|
||||
<div className={`flex-grow flex items-center gap-4`}>
|
||||
<span className='w-full max-w-32'>{isNewProviderName ? displayInfoOfProviderName(providerName).title : ''}</span>
|
||||
<span className='w-fit truncate'>{modelName}</span>
|
||||
{/* <span>{`${modelName} (${providerName})`}</span> */}
|
||||
</div>
|
||||
{/* right part is anything that fits */}
|
||||
<div className='w-fit flex items-center gap-4'>
|
||||
<span className='opacity-50'>{isAutodetected ? '(detected locally)' : isDefault ? '' : '(custom model)'}</span>
|
||||
<div className='flex items-center gap-4'>
|
||||
<span className='opacity-50 truncate'>{isAutodetected ? '(detected locally)' : isDefault ? '' : '(custom model)'}</span>
|
||||
|
||||
<VoidSwitch
|
||||
value={disabled ? false : !isHidden}
|
||||
onChange={() => { settingsStateService.toggleModelHidden(providerName, modelName) }}
|
||||
onChange={() => {
|
||||
settingsStateService.toggleModelHidden(providerName, modelName)
|
||||
}}
|
||||
disabled={disabled}
|
||||
size='sm'
|
||||
/>
|
||||
@@ -235,14 +260,15 @@ const ProviderSetting = ({ providerName, settingName }: { providerName: Provider
|
||||
|
||||
const accessor = useAccessor()
|
||||
const voidSettingsService = accessor.get('IVoidSettingsService')
|
||||
const voidMetricsService = accessor.get('IMetricsService')
|
||||
|
||||
let weChangedTextRef = false
|
||||
|
||||
return <ErrorBoundary>
|
||||
<div className='my-1'>
|
||||
<VoidInputBox
|
||||
// placeholder={`${providerTitle} ${settingTitle} (${placeholder}).`}
|
||||
placeholder={`${settingTitle} (${placeholder}).`}
|
||||
// placeholder={`${providerTitle} ${settingTitle} (${placeholder})`}
|
||||
placeholder={`${settingTitle} (${placeholder})`}
|
||||
onChangeText={useCallback((newVal) => {
|
||||
if (weChangedTextRef) return
|
||||
voidSettingsService.setSettingOfProvider(providerName, settingName, newVal)
|
||||
@@ -268,10 +294,12 @@ const ProviderSetting = ({ providerName, settingName }: { providerName: Provider
|
||||
|
||||
if (shouldEnable) {
|
||||
voidSettingsService.setSettingOfProvider(providerName, '_enabled', true)
|
||||
voidMetricsService.capture('Enable Provider', { providerName })
|
||||
}
|
||||
|
||||
if (shouldDisable) {
|
||||
voidSettingsService.setSettingOfProvider(providerName, '_enabled', false)
|
||||
voidMetricsService.capture('Disable Provider', { providerName })
|
||||
}
|
||||
|
||||
}
|
||||
@@ -281,8 +309,8 @@ const ProviderSetting = ({ providerName, settingName }: { providerName: Provider
|
||||
}, [voidSettingsService, providerName, settingName])}
|
||||
multiline={false}
|
||||
/>
|
||||
{subTextMd === undefined ? null : <div className='py-1 px-3 opacity-50 text-xs'>
|
||||
<ChatMarkdownRender string={subTextMd} />
|
||||
{subTextMd === undefined ? null : <div className='py-1 px-3 opacity-50 text-sm'>
|
||||
<ChatMarkdownRender noSpace string={subTextMd} />
|
||||
</div>}
|
||||
|
||||
</div>
|
||||
@@ -334,61 +362,257 @@ export const VoidProviderSettings = ({ providerNames }: { providerNames: Provide
|
||||
</>
|
||||
}
|
||||
|
||||
// export const VoidFeatureFlagSettings = () => {
|
||||
// const accessor = useAccessor()
|
||||
// const voidSettingsService = accessor.get('IVoidSettingsService')
|
||||
|
||||
// const voidSettingsState = useSettingsState()
|
||||
|
||||
// return <>
|
||||
// {featureFlagNames.map((flagName) => {
|
||||
// const value = voidSettingsState.featureFlagSettings[flagName]
|
||||
// const { description } = displayInfoOfFeatureFlag(flagName)
|
||||
// return <div key={flagName} className='hover:bg-black/10 hover:dark:bg-gray-200/10 rounded-sm overflow-hidden py-1 px-3 my-1'>
|
||||
// <div className='flex items-center'>
|
||||
// <VoidCheckBox
|
||||
// label=''
|
||||
// value={value}
|
||||
// onClick={() => { voidSettingsService.setFeatureFlag(flagName, !value) }}
|
||||
// />
|
||||
// <h4 className='text-sm'>{description}</h4>
|
||||
// </div>
|
||||
// </div>
|
||||
// })}
|
||||
// </>
|
||||
// }
|
||||
export const VoidFeatureFlagSettings = () => {
|
||||
type TabName = 'models' | 'general'
|
||||
export const AutoRefreshToggle = () => {
|
||||
const settingName: GlobalSettingName = 'autoRefreshModels'
|
||||
|
||||
const accessor = useAccessor()
|
||||
const voidSettingsService = accessor.get('IVoidSettingsService')
|
||||
const metricsService = accessor.get('IMetricsService')
|
||||
|
||||
const voidSettingsState = useSettingsState()
|
||||
|
||||
return featureFlagNames.map((flagName) => {
|
||||
// right now this is just `enabled_autoRefreshModels`
|
||||
const enabled = voidSettingsState.globalSettings[settingName]
|
||||
|
||||
// right now this is just `enabled_autoRefreshModels`
|
||||
const enabled = voidSettingsState.featureFlagSettings[flagName]
|
||||
const { description } = displayInfoOfFeatureFlag(flagName)
|
||||
|
||||
return <SubtleButton key={flagName}
|
||||
onClick={() => { voidSettingsService.setFeatureFlag(flagName, !enabled) }}
|
||||
text={description}
|
||||
icon={enabled ? <Check className='stroke-green-500 size-3' /> : <X className='stroke-red-500 size-3' />}
|
||||
disabled={false}
|
||||
/>
|
||||
})
|
||||
return <SubtleButton
|
||||
onClick={() => {
|
||||
voidSettingsService.setGlobalSetting(settingName, !enabled)
|
||||
metricsService.capture('Click', { action: 'Autorefresh Toggle', settingName, enabled: !enabled })
|
||||
}}
|
||||
text={`Automatically detect local providers and models (${refreshableProviderNames.map(providerName => displayInfoOfProviderName(providerName).title).join(', ')}).`}
|
||||
icon={enabled ? <Check className='stroke-green-500 size-3' /> : <X className='stroke-red-500 size-3' />}
|
||||
disabled={false}
|
||||
/>
|
||||
}
|
||||
|
||||
export const AIInstructionsBox = () => {
|
||||
const accessor = useAccessor()
|
||||
const voidSettingsService = accessor.get('IVoidSettingsService')
|
||||
const voidSettingsState = useSettingsState()
|
||||
return <VoidInputBox2
|
||||
className='min-h-[81px] p-3 rounded-sm'
|
||||
initValue={voidSettingsState.globalSettings.aiInstructions}
|
||||
placeholder={`Do not change my indentation or delete my comments. When writing TS or JS, do not add ;'s. Respond to all queries in French. `}
|
||||
multiline
|
||||
onChangeText={(newText) => {
|
||||
voidSettingsService.setGlobalSetting('aiInstructions', newText)
|
||||
}}
|
||||
/>
|
||||
}
|
||||
|
||||
export const FeaturesTab = () => {
|
||||
return <>
|
||||
<h2 className={`text-3xl mb-2`}>Local Providers</h2>
|
||||
{/* <h3 className={`opacity-50 mb-2`}>{`Keep your data private by hosting AI locally on your computer.`}</h3> */}
|
||||
{/* <h3 className={`opacity-50 mb-2`}>{`Instructions:`}</h3> */}
|
||||
{/* <h3 className={`mb-2`}>{`Void can access any model that you host locally. We automatically detect your local models by default.`}</h3> */}
|
||||
<h3 className={`text-void-fg-3 mb-2`}>{`Void can access any model that you host locally. We automatically detect your local models by default.`}</h3>
|
||||
<div className='pl-4 opacity-50'>
|
||||
<span className={`text-sm mb-2`}><ChatMarkdownRender noSpace string={`1. Download [Ollama](https://ollama.com/download).`} /></span>
|
||||
<span className={`text-sm mb-2`}><ChatMarkdownRender noSpace string={`2. Open your terminal.`} /></span>
|
||||
<span className={`text-sm mb-2 select-text`}><ChatMarkdownRender noSpace string={`3. Run \`ollama run llama3.1\`. This installs Meta's llama3.1 model which is best for chat and inline edits. Requires 5GB of memory.`} /></span>
|
||||
<span className={`text-sm mb-2 select-text`}><ChatMarkdownRender noSpace string={`4. Run \`ollama run qwen2.5-coder:1.5b\`. This installs a faster autocomplete model. Requires 1GB of memory.`} /></span>
|
||||
<span className={`text-sm mb-2`}><ChatMarkdownRender noSpace string={`Void automatically detects locally running models and enables them.`} /></span>
|
||||
{/* TODO we should create UI for downloading models without user going into terminal */}
|
||||
</div>
|
||||
|
||||
<ErrorBoundary>
|
||||
<VoidProviderSettings providerNames={localProviderNames} />
|
||||
</ErrorBoundary>
|
||||
|
||||
<h2 className={`text-3xl mb-2 mt-12`}>Providers</h2>
|
||||
<h3 className={`text-void-fg-3 mb-2`}>{`Void can access models from Anthropic, OpenAI, OpenRouter, and more.`}</h3>
|
||||
{/* <h3 className={`opacity-50 mb-2`}>{`Access models like ChatGPT and Claude. We recommend using Anthropic or OpenAI as providers, or Groq as a faster alternative.`}</h3> */}
|
||||
<ErrorBoundary>
|
||||
<VoidProviderSettings providerNames={nonlocalProviderNames} />
|
||||
</ErrorBoundary>
|
||||
|
||||
<h2 className={`text-3xl mb-2 mt-12`}>Models</h2>
|
||||
<ErrorBoundary>
|
||||
<AutoRefreshToggle />
|
||||
<RefreshableModels />
|
||||
<ModelDump />
|
||||
<AddModelMenuFull />
|
||||
</ErrorBoundary>
|
||||
</>
|
||||
}
|
||||
|
||||
|
||||
|
||||
// https://github.com/VSCodium/vscodium/blob/master/docs/index.md#migrating-from-visual-studio-code-to-vscodium
|
||||
// https://code.visualstudio.com/docs/editor/extension-marketplace#_where-are-extensions-installed
|
||||
type TransferFilesInfo = { from: URI, to: URI }[]
|
||||
const transferTheseFilesOfOS = (os: 'mac' | 'windows' | 'linux' | null): TransferFilesInfo => {
|
||||
if (os === null)
|
||||
throw new Error(`One-click switch is not possible in this environment.`)
|
||||
if (os === 'mac') {
|
||||
const homeDir = env['HOME']
|
||||
if (!homeDir) throw new Error(`$HOME not found`)
|
||||
return [{
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Code', 'User', 'settings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'settings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Code', 'User', 'keybindings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'keybindings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.vscode', 'extensions'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'),
|
||||
}]
|
||||
}
|
||||
|
||||
if (os === 'linux') {
|
||||
const homeDir = env['HOME']
|
||||
if (!homeDir) throw new Error(`variable for $HOME location not found`)
|
||||
return [{
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Code', 'User', 'settings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'settings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Code', 'User', 'keybindings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'keybindings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.vscode', 'extensions'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'),
|
||||
}]
|
||||
}
|
||||
|
||||
if (os === 'windows') {
|
||||
const appdata = env['APPDATA']
|
||||
if (!appdata) throw new Error(`variable for %APPDATA% location not found`)
|
||||
const userprofile = env['USERPROFILE']
|
||||
if (!userprofile) throw new Error(`variable for %USERPROFILE% location not found`)
|
||||
|
||||
return [{
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Code', 'User', 'settings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'settings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Code', 'User', 'keybindings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'keybindings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.vscode', 'extensions'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.void-editor', 'extensions'),
|
||||
}]
|
||||
}
|
||||
|
||||
throw new Error(`os '${os}' not recognized`)
|
||||
}
|
||||
|
||||
const os = isWindows ? 'windows' : isMacintosh ? 'mac' : isLinux ? 'linux' : null
|
||||
let transferTheseFiles: TransferFilesInfo = []
|
||||
let transferError: string | null = null
|
||||
|
||||
try { transferTheseFiles = transferTheseFilesOfOS(os) }
|
||||
catch (e) { transferError = e + '' }
|
||||
|
||||
const OneClickSwitchButton = () => {
|
||||
const accessor = useAccessor()
|
||||
const fileService = accessor.get('IFileService')
|
||||
|
||||
const [state, setState] = useState<{ type: 'done', error?: string } | { type: | 'loading' | 'justfinished' }>({ type: 'done' })
|
||||
|
||||
if (transferTheseFiles.length === 0)
|
||||
return <>
|
||||
<WarningBox text={transferError ?? `One-click switch not available.`} />
|
||||
</>
|
||||
|
||||
|
||||
|
||||
const onClick = async () => {
|
||||
|
||||
if (state.type !== 'done') return
|
||||
|
||||
setState({ type: 'loading' })
|
||||
|
||||
let errAcc = ''
|
||||
for (let { from, to } of transferTheseFiles) {
|
||||
console.log('transferring', from, to)
|
||||
// not sure if this can fail, just wrapping it with try/catch for now
|
||||
try { await fileService.copy(from, to, true) }
|
||||
catch (e) { errAcc += e + '\n' }
|
||||
}
|
||||
const hadError = !!errAcc
|
||||
if (hadError) {
|
||||
setState({ type: 'done', error: errAcc })
|
||||
}
|
||||
else {
|
||||
setState({ type: 'justfinished' })
|
||||
setTimeout(() => { setState({ type: 'done' }); }, 3000)
|
||||
}
|
||||
}
|
||||
|
||||
return <>
|
||||
<VoidButton disabled={state.type !== 'done'} onClick={onClick}>
|
||||
{state.type === 'done' ? 'Transfer my Settings'
|
||||
: state.type === 'loading' ? 'Transferring...'
|
||||
: state.type === 'justfinished' ? 'Success!'
|
||||
: null
|
||||
}
|
||||
</VoidButton>
|
||||
{state.type === 'done' && state.error ? <WarningBox text={state.error} /> : null}
|
||||
</>
|
||||
}
|
||||
|
||||
|
||||
const GeneralTab = () => {
|
||||
const accessor = useAccessor()
|
||||
const commandService = accessor.get('ICommandService')
|
||||
|
||||
return <>
|
||||
|
||||
|
||||
<div className=''>
|
||||
<h2 className={`text-3xl mb-2`}>One-Click Switch</h2>
|
||||
<h4 className={`text-void-fg-3 mb-2`}>{`Transfer your settings from VS Code to Void in one click.`}</h4>
|
||||
<OneClickSwitchButton />
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div className='mt-12'>
|
||||
<h2 className={`text-3xl mb-2`}>Built-in Settings</h2>
|
||||
<h4 className={`text-void-fg-3 mb-2`}>{`IDE settings, keyboard settings, and theme customization.`}</h4>
|
||||
|
||||
<div className='my-4'>
|
||||
<VoidButton onClick={() => { commandService.executeCommand('workbench.action.openSettings') }}>
|
||||
General Settings
|
||||
</VoidButton>
|
||||
</div>
|
||||
<div className='my-4'>
|
||||
<VoidButton onClick={() => { commandService.executeCommand('workbench.action.openGlobalKeybindings') }}>
|
||||
Keyboard Settings
|
||||
</VoidButton>
|
||||
</div>
|
||||
<div className='my-4'>
|
||||
<VoidButton onClick={() => { commandService.executeCommand('workbench.action.selectTheme') }}>
|
||||
Theme Settings
|
||||
</VoidButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className='mt-12'>
|
||||
<h2 className={`text-3xl mb-2`}>AI Instructions</h2>
|
||||
<h4 className={`text-void-fg-3 mb-2`}>{`Instructions to include on all AI requests.`}</h4>
|
||||
<AIInstructionsBox />
|
||||
</div>
|
||||
|
||||
|
||||
</>
|
||||
}
|
||||
|
||||
// full settings
|
||||
|
||||
export const Settings = () => {
|
||||
const isDark = useIsDark()
|
||||
|
||||
const [tab, setTab] = useState<'models' | 'features'>('models')
|
||||
const [tab, setTab] = useState<TabName>('models')
|
||||
|
||||
return <div className={`@@void-scope ${isDark ? 'dark' : ''}`}>
|
||||
<div className='w-full h-full px-10 py-10 select-none'>
|
||||
const containerRef = useRef<HTMLDivElement | null>(null)
|
||||
useScrollbarStyles(containerRef)
|
||||
|
||||
return <div className={`@@void-scope ${isDark ? 'dark' : ''}`} style={{ height: '100%', width: '100%' }}>
|
||||
<div ref={containerRef} className='overflow-y-auto w-full h-full px-10 py-10 select-none'>
|
||||
|
||||
<div className='max-w-5xl mx-auto'>
|
||||
|
||||
@@ -404,9 +628,9 @@ export const Settings = () => {
|
||||
<button className={`text-left p-1 px-3 my-0.5 rounded-sm overflow-hidden ${tab === 'models' ? 'bg-black/10 dark:bg-gray-200/10' : ''} hover:bg-black/10 hover:dark:bg-gray-200/10 active:bg-black/10 active:dark:bg-gray-200/10 `}
|
||||
onClick={() => { setTab('models') }}
|
||||
>Models</button>
|
||||
{/* <button className={`text-left p-1 px-3 my-0.5 rounded-sm overflow-hidden ${tab === 'features' ? 'bg-black/10 dark:bg-gray-200/10' : ''} hover:bg-black/10 hover:dark:bg-gray-200/10 active:bg-black/10 active:dark:bg-gray-200/10 `}
|
||||
onClick={() => { setTab('features') }}
|
||||
>Features</button> */}
|
||||
<button className={`text-left p-1 px-3 my-0.5 rounded-sm overflow-hidden ${tab === 'general' ? 'bg-black/10 dark:bg-gray-200/10' : ''} hover:bg-black/10 hover:dark:bg-gray-200/10 active:bg-black/10 active:dark:bg-gray-200/10 `}
|
||||
onClick={() => { setTab('general') }}
|
||||
>General</button>
|
||||
</div>
|
||||
|
||||
{/* separator */}
|
||||
@@ -414,44 +638,14 @@ export const Settings = () => {
|
||||
|
||||
|
||||
{/* content */}
|
||||
<div className='w-full overflow-y-auto'>
|
||||
<div className='w-full min-w-[600px] overflow-auto'>
|
||||
|
||||
<div className={`${tab !== 'models' ? 'hidden' : ''}`}>
|
||||
<h2 className={`text-3xl mb-2`}>Local Providers</h2>
|
||||
{/* <h3 className={`text-md opacity-50 mb-2`}>{`Keep your data private by hosting AI locally on your computer.`}</h3> */}
|
||||
{/* <h3 className={`text-md opacity-50 mb-2`}>{`Instructions:`}</h3> */}
|
||||
<h3 className={`text-md mb-2`}>{`Void can access any model that you host locally. We automatically detect your local models by default.`}</h3>
|
||||
<div className='pl-4 select-text opacity-50'>
|
||||
<h4 className={`text-xs mb-2`}><ChatMarkdownRender string={`1. Download [Ollama](https://ollama.com/download).`} /></h4>
|
||||
<h4 className={`text-xs mb-2`}><ChatMarkdownRender string={`2. Open your terminal.`} /></h4>
|
||||
<h4 className={`text-xs mb-2`}><ChatMarkdownRender string={`3. Run \`ollama run llama3.1\`. This installs Meta's llama model which is competitive with GPT-series models. It requires 5GB of memory.`} /></h4>
|
||||
<h4 className={`text-xs mb-2`}><ChatMarkdownRender string={`4. Run \`ollama run qwen2.5-coder:1.5b\`. This is a faster autocomplete model and requires 1GB of memory.`} /></h4>
|
||||
{/* TODO we should create UI for downloading models without user going into terminal */}
|
||||
</div>
|
||||
|
||||
<ErrorBoundary>
|
||||
<VoidProviderSettings providerNames={localProviderNames} />
|
||||
</ErrorBoundary>
|
||||
|
||||
<h2 className={`text-3xl mb-2 mt-16`}>More Providers</h2>
|
||||
<h3 className={`text-md mb-2`}>{`Void can also access models like ChatGPT and Claude. We recommend using Anthropic or OpenAI.`}</h3>
|
||||
{/* <h3 className={`text-md opacity-50 mb-2`}>{`Access models like ChatGPT and Claude. We recommend using Anthropic or OpenAI as providers, or Groq as a faster alternative.`}</h3> */}
|
||||
<ErrorBoundary>
|
||||
<VoidProviderSettings providerNames={nonlocalProviderNames} />
|
||||
</ErrorBoundary>
|
||||
|
||||
<h2 className={`text-3xl mb-2 mt-16`}>Models</h2>
|
||||
<ErrorBoundary>
|
||||
<VoidFeatureFlagSettings />
|
||||
<RefreshableModels />
|
||||
<ModelDump />
|
||||
<AddModelMenuFull />
|
||||
</ErrorBoundary>
|
||||
<FeaturesTab />
|
||||
</div>
|
||||
|
||||
<div className={`${tab !== 'features' ? 'hidden' : ''}`}>
|
||||
<h2 className={`text-3xl mb-2`} onClick={() => { setTab('features') }}>Features</h2>
|
||||
{/* <VoidFeatureFlagSettings /> */}
|
||||
<div className={`${tab !== 'general' ? 'hidden' : ''}`}>
|
||||
<GeneralTab />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*------------------------------------------------------------------------------------------
|
||||
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
|
||||
*-----------------------------------------------------------------------------------------*/
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* 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 { Settings } from './Settings.js'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*------------------------------------------------------------------------------------------
|
||||
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
|
||||
*-----------------------------------------------------------------------------------------*/
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
@@ -9,7 +9,38 @@ module.exports = {
|
||||
content: ['./src2/**/*.{jsx,tsx}'], // uses these files to decide how to transform the css file
|
||||
theme: {
|
||||
extend: {
|
||||
fontSize: {
|
||||
xs: '10px',
|
||||
sm: '11px',
|
||||
root: '13px',
|
||||
lg: '14px',
|
||||
xl: '16px',
|
||||
'2xl': '18px',
|
||||
'3xl': '20px',
|
||||
'4xl': '24px',
|
||||
'5xl': '30px',
|
||||
'6xl': '36px',
|
||||
'7xl': '48px',
|
||||
'8xl': '64px',
|
||||
'9xl': '72px',
|
||||
},
|
||||
// common colors to use, ordered light to dark
|
||||
|
||||
colors: {
|
||||
"void-bg-1": "var(--vscode-input-background)",
|
||||
"void-bg-2": "var(--vscode-sideBar-background)",
|
||||
"void-bg-3": "var(--vscode-editor-background)",
|
||||
|
||||
"void-fg-1": "var(--vscode-editor-foreground)",
|
||||
"void-fg-2": "var(--vscode-input-foreground)",
|
||||
"void-fg-3": "var(--vscode-input-placeholderForeground)",
|
||||
"void-warning": "var(--vscode-charts-yellow)",
|
||||
|
||||
"void-border-1": "var(--vscode-commandCenter-activeBorder)",
|
||||
"void-border-2": "var(--vscode-commandCenter-border)",
|
||||
"void-border-3": "var(--vscode-commandCenter-inactiveBorder)",
|
||||
|
||||
|
||||
vscode: {
|
||||
// see: https://code.visualstudio.com/api/extension-guides/webview#theming-webview-content
|
||||
|
||||
@@ -39,8 +70,8 @@ module.exports = {
|
||||
"input-bg": "var(--vscode-input-background)",
|
||||
"input-border": "var(--vscode-input-border)",
|
||||
"input-fg": "var(--vscode-input-foreground)",
|
||||
"input-placeholder-fg": "var(--vscode-placeholderForeground)",
|
||||
"input-active-bg": "var(--vscode-activeBackground)",
|
||||
"input-placeholder-fg": "var(--vscode-input-placeholderForeground)",
|
||||
"input-active-bg": "var(--vscode-input-activeBackground)",
|
||||
"input-option-active-border": "var(--vscode-inputOption-activeBorder)",
|
||||
"input-option-active-fg": "var(--vscode-inputOption-activeForeground)",
|
||||
"input-option-hover-bg": "var(--vscode-inputOption-hoverBackground)",
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
/*------------------------------------------------------------------------------------------
|
||||
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
|
||||
*-----------------------------------------------------------------------------------------*/
|
||||
|
||||
{
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"exactOptionalPropertyTypes": false,
|
||||
@@ -17,5 +16,10 @@
|
||||
// this is just for type checking, so src/ is the correct dir
|
||||
"./src/**/*.ts",
|
||||
"./src/**/*.tsx"
|
||||
]
|
||||
],
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*------------------------------------------------------------------------------------------
|
||||
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
|
||||
*-----------------------------------------------------------------------------------------*/
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { defineConfig } from 'tsup'
|
||||
|
||||
@@ -9,7 +9,7 @@ export default defineConfig({
|
||||
entry: [
|
||||
'./src2/sidebar-tsx/index.tsx',
|
||||
'./src2/void-settings-tsx/index.tsx',
|
||||
'./src2/ctrl-k-tsx/index.tsx',
|
||||
'./src2/quick-edit-tsx/index.tsx',
|
||||
'./src2/diff/index.tsx',
|
||||
],
|
||||
outDir: './out',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*------------------------------------------------------------------------------------------
|
||||
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
|
||||
*-----------------------------------------------------------------------------------------*/
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* 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';
|
||||
|
||||
@@ -11,24 +11,40 @@ import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js
|
||||
|
||||
import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';
|
||||
import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js';
|
||||
import { CodeStagingSelection, IThreadHistoryService } from './threadHistoryService.js';
|
||||
import { CodeStagingSelection, IChatThreadService } from './chatThreadService.js';
|
||||
|
||||
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
|
||||
import { IRange } from '../../../../editor/common/core/range.js';
|
||||
import { ITextModel } from '../../../../editor/common/model.js';
|
||||
import { VOID_VIEW_ID } from './sidebarPane.js';
|
||||
import { VOID_VIEW_CONTAINER_ID, VOID_VIEW_ID } from './sidebarPane.js';
|
||||
import { IMetricsService } from '../../../../platform/void/common/metricsService.js';
|
||||
import { ISidebarStateService } from './sidebarStateService.js';
|
||||
import { ICommandService } from '../../../../platform/commands/common/commands.js';
|
||||
import { VOID_OPEN_SETTINGS_ACTION_ID } from './voidSettingsPane.js';
|
||||
|
||||
import { VOID_TOGGLE_SETTINGS_ACTION_ID } from './voidSettingsPane.js';
|
||||
import { VOID_CTRL_L_ACTION_ID } from './actionIDs.js';
|
||||
import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js';
|
||||
import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js';
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { localize2 } from '../../../../nls.js';
|
||||
import { IViewsService } from '../../../services/views/common/viewsService.js';
|
||||
import { IVoidUriStateService } from './voidUriStateService.js';
|
||||
|
||||
// ---------- Register commands and keybindings ----------
|
||||
|
||||
|
||||
const roundRangeToLines = (range: IRange | null | undefined) => {
|
||||
export const roundRangeToLines = (range: IRange | null | undefined, options: { emptySelectionBehavior: 'null' | 'line' }) => {
|
||||
if (!range)
|
||||
return null
|
||||
|
||||
// treat as no selection if selection is empty
|
||||
if (range.endColumn === range.startColumn && range.endLineNumber === range.startLineNumber) {
|
||||
if (options.emptySelectionBehavior === 'null')
|
||||
return null
|
||||
else if (options.emptySelectionBehavior === 'line')
|
||||
return { startLineNumber: range.startLineNumber, startColumn: 1, endLineNumber: range.startLineNumber, endColumn: 1 }
|
||||
}
|
||||
|
||||
// IRange is 1-indexed
|
||||
const endLine = range.endColumn === 1 ? range.endLineNumber - 1 : range.endLineNumber // e.g. if the user triple clicks, it selects column=0, line=line -> column=0, line=line+1
|
||||
const newRange: IRange = {
|
||||
@@ -50,11 +66,28 @@ const getContentInRange = (model: ITextModel, range: IRange | null) => {
|
||||
return trimmedContent
|
||||
}
|
||||
|
||||
// Action: when press ctrl+L, show the sidebar chat and add to the selection
|
||||
export const VOID_CTRL_L_ACTION_ID = 'void.ctrlLAction'
|
||||
|
||||
|
||||
const VOID_OPEN_SIDEBAR_ACTION_ID = 'void.sidebar.open'
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({ id: VOID_CTRL_L_ACTION_ID, title: 'Void: Show Sidebar', keybinding: { primary: KeyMod.CtrlCmd | KeyCode.KeyL, weight: KeybindingWeight.BuiltinExtension } });
|
||||
super({ id: VOID_OPEN_SIDEBAR_ACTION_ID, title: localize2('voidOpenSidebar', 'Void: Open Sidebar'), f1: true });
|
||||
}
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const stateService = accessor.get(ISidebarStateService)
|
||||
stateService.setState({ isHistoryOpen: false, currentTab: 'chat' })
|
||||
stateService.fireFocusChat()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
// Action: when press ctrl+L, show the sidebar chat and add to the selection
|
||||
const VOID_ADD_SELECTION_TO_SIDEBAR_ACTION_ID = 'void.sidebar.select'
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({ id: VOID_ADD_SELECTION_TO_SIDEBAR_ACTION_ID, title: localize2('voidAddToSidebar', 'Void: Add Selection to Sidebar'), f1: true });
|
||||
}
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
|
||||
@@ -62,67 +95,84 @@ registerAction2(class extends Action2 {
|
||||
if (!model)
|
||||
return
|
||||
|
||||
const stateService = accessor.get(ISidebarStateService)
|
||||
const metricsService = accessor.get(IMetricsService)
|
||||
const editorService = accessor.get(ICodeEditorService)
|
||||
|
||||
metricsService.capture('User Action', { type: 'Ctrl+L' })
|
||||
|
||||
stateService.setState({ isHistoryOpen: false, currentTab: 'chat' })
|
||||
stateService.fireFocusChat()
|
||||
metricsService.capture('Ctrl+L', {})
|
||||
|
||||
const editor = editorService.getActiveCodeEditor()
|
||||
const selectionRange = roundRangeToLines(
|
||||
// accessor.get(IEditorService).activeTextEditorControl?.getSelection()
|
||||
editor?.getSelection()
|
||||
// accessor.get(IEditorService).activeTextEditorControl?.getSelection()
|
||||
const selectionRange = roundRangeToLines(editor?.getSelection(), { emptySelectionBehavior: 'null' })
|
||||
|
||||
|
||||
// select whole lines
|
||||
if (selectionRange) {
|
||||
editor?.setSelection({ startLineNumber: selectionRange.startLineNumber, endLineNumber: selectionRange.endLineNumber, startColumn: 1, endColumn: Number.MAX_SAFE_INTEGER })
|
||||
}
|
||||
|
||||
const selectionStr = getContentInRange(model, selectionRange)
|
||||
|
||||
const selection: CodeStagingSelection = !selectionRange || !selectionStr || (selectionRange.startLineNumber > selectionRange.endLineNumber) ? {
|
||||
type: 'File',
|
||||
fileURI: model.uri,
|
||||
selectionStr: null,
|
||||
range: null,
|
||||
} : {
|
||||
type: 'Selection',
|
||||
fileURI: model.uri,
|
||||
selectionStr: selectionStr,
|
||||
range: selectionRange,
|
||||
}
|
||||
|
||||
// add selection to staging
|
||||
const chatThreadService = accessor.get(IChatThreadService)
|
||||
const currentStaging = chatThreadService.state.currentStagingSelections
|
||||
const currentStagingEltIdx = currentStaging?.findIndex(s =>
|
||||
s.fileURI.fsPath === model.uri.fsPath
|
||||
&& s.range?.startLineNumber === selection.range?.startLineNumber
|
||||
&& s.range?.endLineNumber === selection.range?.endLineNumber
|
||||
)
|
||||
|
||||
|
||||
if (selectionRange) {
|
||||
// select whole lines
|
||||
editor?.setSelection({ startLineNumber: selectionRange.startLineNumber, endLineNumber: selectionRange.endLineNumber, startColumn: 1, endColumn: Number.MAX_SAFE_INTEGER })
|
||||
|
||||
const selectionStr = getContentInRange(model, selectionRange)
|
||||
|
||||
const selection: CodeStagingSelection = selectionStr === null || selectionRange.startLineNumber > selectionRange.endLineNumber ? {
|
||||
type: 'File',
|
||||
fileURI: model.uri,
|
||||
selectionStr: null,
|
||||
range: null,
|
||||
} : {
|
||||
type: 'Selection',
|
||||
fileURI: model.uri,
|
||||
selectionStr: selectionStr,
|
||||
range: selectionRange,
|
||||
}
|
||||
|
||||
// add selection to staging
|
||||
const threadHistoryService = accessor.get(IThreadHistoryService)
|
||||
const currentStaging = threadHistoryService.state._currentStagingSelections
|
||||
const currentStagingEltIdx = currentStaging?.findIndex(s =>
|
||||
s.fileURI.fsPath === model.uri.fsPath
|
||||
&& s.range?.startLineNumber === selection.range?.startLineNumber
|
||||
&& s.range?.endLineNumber === selection.range?.endLineNumber
|
||||
)
|
||||
|
||||
// if matches with existing selection, overwrite
|
||||
if (currentStagingEltIdx !== undefined && currentStagingEltIdx !== -1) {
|
||||
threadHistoryService.setStaging([
|
||||
...currentStaging!.slice(0, currentStagingEltIdx),
|
||||
selection,
|
||||
...currentStaging!.slice(currentStagingEltIdx + 1, Infinity)
|
||||
])
|
||||
}
|
||||
// if no match, add
|
||||
else {
|
||||
threadHistoryService.setStaging([...(currentStaging ?? []), selection])
|
||||
}
|
||||
// if matches with existing selection, overwrite
|
||||
if (currentStagingEltIdx !== undefined && currentStagingEltIdx !== -1) {
|
||||
chatThreadService.setStaging([
|
||||
...currentStaging!.slice(0, currentStagingEltIdx),
|
||||
selection,
|
||||
...currentStaging!.slice(currentStagingEltIdx + 1, Infinity)
|
||||
])
|
||||
}
|
||||
// if no match, add
|
||||
else {
|
||||
chatThreadService.setStaging([...(currentStaging ?? []), selection])
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: VOID_CTRL_L_ACTION_ID,
|
||||
f1: true,
|
||||
title: localize2('voidCtrlL', 'Void: Add Select to Chat'),
|
||||
keybinding: {
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KeyL,
|
||||
weight: KeybindingWeight.VoidExtension
|
||||
}
|
||||
});
|
||||
}
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const commandService = accessor.get(ICommandService)
|
||||
await commandService.executeCommand(VOID_OPEN_SIDEBAR_ACTION_ID)
|
||||
await commandService.executeCommand(VOID_ADD_SELECTION_TO_SIDEBAR_ACTION_ID)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// New chat menu button
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
@@ -141,8 +191,8 @@ registerAction2(class extends Action2 {
|
||||
|
||||
stateService.setState({ isHistoryOpen: false, currentTab: 'chat' })
|
||||
stateService.fireFocusChat()
|
||||
const historyService = accessor.get(IThreadHistoryService)
|
||||
historyService.startNewThread()
|
||||
const chatThreadService = accessor.get(IChatThreadService)
|
||||
chatThreadService.openNewThread()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -180,6 +230,75 @@ registerAction2(class extends Action2 {
|
||||
}
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const commandService = accessor.get(ICommandService)
|
||||
commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID)
|
||||
commandService.executeCommand(VOID_TOGGLE_SETTINGS_ACTION_ID)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
export class TabSwitchListener extends Disposable {
|
||||
|
||||
constructor(
|
||||
onSwitchTab: () => void,
|
||||
@ICodeEditorService private readonly _editorService: ICodeEditorService,
|
||||
) {
|
||||
super()
|
||||
|
||||
// when editor switches tabs (models)
|
||||
const addTabSwitchListeners = (editor: ICodeEditor) => {
|
||||
this._register(editor.onDidChangeModel(e => {
|
||||
if (e.newModelUrl?.scheme !== 'file') return
|
||||
onSwitchTab()
|
||||
}))
|
||||
}
|
||||
|
||||
const initializeEditor = (editor: ICodeEditor) => {
|
||||
addTabSwitchListeners(editor)
|
||||
}
|
||||
|
||||
// initialize current editors + any new editors
|
||||
for (let editor of this._editorService.listCodeEditors()) initializeEditor(editor)
|
||||
this._register(this._editorService.onCodeEditorAdd(editor => { initializeEditor(editor) }))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class TabSwitchContribution extends Disposable implements IWorkbenchContribution {
|
||||
static readonly ID = 'workbench.contrib.void.tabswitch'
|
||||
|
||||
constructor(
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
@IViewsService private readonly viewsService: IViewsService,
|
||||
@IVoidUriStateService private readonly uriStateService: IVoidUriStateService,
|
||||
@ICodeEditorService private readonly codeEditorService: ICodeEditorService,
|
||||
// @ICommandService private readonly commandService: ICommandService,
|
||||
) {
|
||||
super()
|
||||
|
||||
// sidebarIsVisible state
|
||||
let sidebarIsVisible = this.viewsService.isViewContainerVisible(VOID_VIEW_CONTAINER_ID)
|
||||
this._register(this.viewsService.onDidChangeViewVisibility(e => {
|
||||
sidebarIsVisible = e.visible
|
||||
}))
|
||||
|
||||
const onSwitchTab = () => { // update state
|
||||
if (sidebarIsVisible) {
|
||||
const currentUri = this.codeEditorService.getActiveCodeEditor()?.getModel()?.uri
|
||||
if (!currentUri) return;
|
||||
this.uriStateService.setState({ currentUri })
|
||||
// this.commandService.executeCommand(VOID_ADD_SELECTION_TO_SIDEBAR_ACTION_ID)
|
||||
}
|
||||
}
|
||||
|
||||
// when sidebar becomes visible, add current file
|
||||
this._register(this.viewsService.onDidChangeViewVisibility(e => { sidebarIsVisible = e.visible }))
|
||||
|
||||
// run on current tab if it exists, and listen for tab switches and visibility changes
|
||||
onSwitchTab()
|
||||
this._register(this.viewsService.onDidChangeViewVisibility(() => { onSwitchTab() }))
|
||||
this._register(this.instantiationService.createInstance(TabSwitchListener, () => { onSwitchTab() }))
|
||||
}
|
||||
}
|
||||
|
||||
registerWorkbenchContribution2(TabSwitchContribution.ID, TabSwitchContribution, WorkbenchPhase.BlockRestore);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*------------------------------------------------------------------------------------------
|
||||
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
|
||||
*-----------------------------------------------------------------------------------------*/
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Registry } from '../../../../platform/registry/common/platform.js';
|
||||
import {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*------------------------------------------------------------------------------------------
|
||||
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
|
||||
*-----------------------------------------------------------------------------------------*/
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Emitter, Event } from '../../../../base/common/event.js';
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
|
||||
@@ -1,213 +0,0 @@
|
||||
/*------------------------------------------------------------------------------------------
|
||||
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
|
||||
*-----------------------------------------------------------------------------------------*/
|
||||
|
||||
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 { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
|
||||
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
import { Emitter, Event } from '../../../../base/common/event.js';
|
||||
import { IAutocompleteService } from './autocompleteService.js';
|
||||
import { IRange } from '../../../../editor/common/core/range.js';
|
||||
|
||||
export type CodeSelection = {
|
||||
fileURI: URI;
|
||||
selectionStr: string | null;
|
||||
content: string; // TODO remove this (replace `selectionStr` with `content`)
|
||||
range: IRange;
|
||||
}
|
||||
|
||||
// if selectionStr is null, it means to use the entire file at send time
|
||||
export type CodeStagingSelection = {
|
||||
type: 'Selection',
|
||||
fileURI: URI,
|
||||
selectionStr: string,
|
||||
range: IRange
|
||||
} | {
|
||||
type: 'File',
|
||||
fileURI: URI,
|
||||
selectionStr: null,
|
||||
range: null
|
||||
}
|
||||
|
||||
|
||||
// WARNING: changing this format is a big deal!!!!!! need to migrate old format to new format on users' computers so people don't get errors.
|
||||
export type ChatMessage =
|
||||
| {
|
||||
role: 'user';
|
||||
content: string | null; // content sent to the llm - allowed to be '', will be replaced with (empty)
|
||||
displayContent: string | null; // content displayed to user - allowed to be '', will be ignored
|
||||
selections: CodeSelection[] | null; // the user's selection
|
||||
}
|
||||
| {
|
||||
role: 'assistant';
|
||||
content: string | null; // content received from LLM - allowed to be '', will be replaced with (empty)
|
||||
displayContent: string | null; // content displayed to user (this is the same as content for now) - allowed to be '', will be ignored
|
||||
}
|
||||
| {
|
||||
role: 'system';
|
||||
content: string;
|
||||
displayContent?: undefined;
|
||||
}
|
||||
|
||||
// a 'thread' means a chat message history
|
||||
export type ChatThreads = {
|
||||
[id: string]: {
|
||||
id: string; // store the id here too
|
||||
createdAt: string; // ISO string
|
||||
lastModified: string; // ISO string
|
||||
messages: ChatMessage[];
|
||||
};
|
||||
}
|
||||
|
||||
export type ThreadsState = {
|
||||
allThreads: ChatThreads;
|
||||
_currentThreadId: string | null; // intended for internal use only
|
||||
_currentStagingSelections: CodeStagingSelection[] | null;
|
||||
}
|
||||
|
||||
|
||||
const newThreadObject = () => {
|
||||
const now = new Date().toISOString()
|
||||
return {
|
||||
id: new Date().getTime().toString(),
|
||||
createdAt: now,
|
||||
lastModified: now,
|
||||
messages: [],
|
||||
}
|
||||
}
|
||||
|
||||
const THREAD_STORAGE_KEY = 'void.threadHistory'
|
||||
|
||||
export interface IThreadHistoryService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
readonly state: ThreadsState;
|
||||
onDidChangeCurrentThread: Event<void>;
|
||||
|
||||
getCurrentThread(state: ThreadsState): ChatThreads[string] | null;
|
||||
startNewThread(): void;
|
||||
switchToThread(threadId: string): void;
|
||||
addMessageToCurrentThread(message: ChatMessage): void;
|
||||
|
||||
setStaging(stagingSelection: CodeStagingSelection[] | null): void;
|
||||
|
||||
}
|
||||
|
||||
export const IThreadHistoryService = createDecorator<IThreadHistoryService>('voidThreadHistoryService');
|
||||
class ThreadHistoryService extends Disposable implements IThreadHistoryService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
// this fires when the current thread changes at all (a switch of currentThread, or a message added to it, etc)
|
||||
private readonly _onDidChangeCurrentThread = new Emitter<void>();
|
||||
readonly onDidChangeCurrentThread: Event<void> = this._onDidChangeCurrentThread.event;
|
||||
|
||||
state: ThreadsState // allThreads is persisted, currentThread is not
|
||||
|
||||
constructor(
|
||||
@IStorageService private readonly _storageService: IStorageService,
|
||||
@IAutocompleteService private readonly _autocomplete: IAutocompleteService,
|
||||
) {
|
||||
super()
|
||||
this._autocomplete
|
||||
|
||||
this.state = {
|
||||
allThreads: this._readAllThreads(),
|
||||
_currentThreadId: null,
|
||||
_currentStagingSelections: null,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private _readAllThreads(): ChatThreads {
|
||||
const threads = this._storageService.get(THREAD_STORAGE_KEY, StorageScope.APPLICATION)
|
||||
return threads ? JSON.parse(threads) : {}
|
||||
}
|
||||
|
||||
private _storeAllThreads(threads: ChatThreads) {
|
||||
this._storageService.store(THREAD_STORAGE_KEY, JSON.stringify(threads), StorageScope.APPLICATION, StorageTarget.USER)
|
||||
}
|
||||
|
||||
// this should be the only place this.state = ... appears besides constructor
|
||||
private _setState(state: Partial<ThreadsState>, affectsCurrent: boolean) {
|
||||
this.state = {
|
||||
...this.state,
|
||||
...state
|
||||
}
|
||||
if (affectsCurrent)
|
||||
this._onDidChangeCurrentThread.fire()
|
||||
}
|
||||
|
||||
// must "prove" that you have access to the current state by providing it
|
||||
getCurrentThread(state: ThreadsState): ChatThreads[string] | null {
|
||||
return state._currentThreadId ? state.allThreads[state._currentThreadId] ?? null : null;
|
||||
}
|
||||
|
||||
switchToThread(threadId: string) {
|
||||
console.log('threadId', threadId)
|
||||
console.log('messages', this.state.allThreads[threadId].messages)
|
||||
this._setState({ _currentThreadId: threadId }, true)
|
||||
}
|
||||
|
||||
|
||||
startNewThread() {
|
||||
// if a thread with 0 messages already exists, switch to it
|
||||
const { allThreads: currentThreads } = this.state
|
||||
for (const threadId in currentThreads) {
|
||||
if (currentThreads[threadId].messages.length === 0) {
|
||||
this.switchToThread(threadId)
|
||||
return
|
||||
}
|
||||
}
|
||||
// otherwise, start a new thread
|
||||
const newThread = newThreadObject()
|
||||
|
||||
// update state
|
||||
const newThreads = {
|
||||
...currentThreads,
|
||||
[newThread.id]: newThread
|
||||
}
|
||||
this._storeAllThreads(newThreads)
|
||||
this._setState({ allThreads: newThreads, _currentThreadId: newThread.id }, true)
|
||||
}
|
||||
|
||||
|
||||
addMessageToCurrentThread(message: ChatMessage) {
|
||||
console.log('adding ', message.role, 'to chat')
|
||||
const { allThreads, _currentThreadId } = this.state
|
||||
|
||||
// get the current thread, or create one
|
||||
let currentThread: ChatThreads[string]
|
||||
if (_currentThreadId && (_currentThreadId in allThreads)) {
|
||||
currentThread = allThreads[_currentThreadId]
|
||||
}
|
||||
else {
|
||||
currentThread = newThreadObject()
|
||||
this.state._currentThreadId = currentThread.id
|
||||
}
|
||||
|
||||
// update state and store it
|
||||
const newThreads = {
|
||||
...allThreads,
|
||||
[currentThread.id]: {
|
||||
...currentThread,
|
||||
lastModified: new Date().toISOString(),
|
||||
messages: [...currentThread.messages, message],
|
||||
}
|
||||
}
|
||||
this._storeAllThreads(newThreads)
|
||||
this._setState({ allThreads: newThreads }, true) // the current thread just changed (it had a message added to it)
|
||||
}
|
||||
|
||||
|
||||
setStaging(stagingSelection: CodeStagingSelection[] | null): void {
|
||||
this._setState({ _currentStagingSelections: stagingSelection }, true) // this is a hack for now
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
registerSingleton(IThreadHistoryService, ThreadHistoryService, InstantiationType.Eager);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*------------------------------------------------------------------------------------------
|
||||
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
|
||||
*-----------------------------------------------------------------------------------------*/
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
|
||||
// register inline diffs
|
||||
@@ -16,7 +16,7 @@ import './sidebarStateService.js'
|
||||
import './quickEditActions.js'
|
||||
|
||||
// register Thread History
|
||||
import './threadHistoryService.js'
|
||||
import './chatThreadService.js'
|
||||
|
||||
// register Autocomplete
|
||||
import './autocompleteService.js'
|
||||
@@ -26,3 +26,6 @@ import './voidSettingsPane.js'
|
||||
|
||||
// register css
|
||||
import './media/void.css'
|
||||
|
||||
// update (frontend part, also see platform/)
|
||||
import './voidUpdateActions.js'
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user