Compare commits

...

134 Commits

Author SHA1 Message Date
Andrew Pareles 7925e438c6 header update 2025-01-09 18:51:40 -08:00
Andrew Pareles 2f5e954ebf update 2025-01-09 18:49:05 -08:00
Andrew Pareles 6a8ac1a68e apache 2.0 2025-01-09 18:48:52 -08:00
Andrew Pareles c5b6fd9b75 Create temp 2025-01-09 18:48:15 -08:00
Raz Luvaton 5083b8e971 fix react build.js to properly find the base folder (#203) 2025-01-05 18:00:40 -08:00
Andrew Pareles 85c2c80db0 Update README.md 2025-01-04 23:55:52 -08:00
Andrew Pareles b677d3dfc1 Update CONTRIBUTING.md 2025-01-03 17:18:18 -08:00
Andrew Pareles cc177b9cd4 update package 2025-01-03 14:14:37 -08:00
Andrew Pareles dd204978e5 update lock 2025-01-03 13:31:54 -08:00
Andrew Pareles fb48c4e67e Update VOID_USEFUL_LINKS.md 2025-01-03 13:15:55 -08:00
Andrew Pareles 74fa5cb505 Merge pull request #202 from PraveenAsh/patch-1
Update CONTRIBUTING.md
2025-01-03 12:44:33 -08:00
Andrew Pareles fae03c3060 Update CONTRIBUTING.md 2025-01-03 12:44:12 -08:00
Praveen Naik 68722ee5c3 Update CONTRIBUTING.md
Added details about Bundling output. Most of us open only the repo without workspace to save memory. It's easy to miss out on where the bundle is located. Added clarity
2025-01-03 21:04:54 +05:30
Andrew Pareles 4a993a716b Update README.md 2025-01-02 23:34:37 -08:00
Andrew Pareles 23d1d4169a update CONTRIBUTING.md 2025-01-02 23:20:36 -08:00
Andrew Pareles f688e1ffae Update LICENSE-VS-Code.txt 2025-01-02 23:06:06 -08:00
Andrew Pareles a443315c14 Merge branch 'main' of https://github.com/voideditor/void 2025-01-02 23:02:05 -08:00
Andrew Pareles eeacb2d3c9 separate licenses + header 2025-01-02 23:00:59 -08:00
Andrew Pareles cdbdf3c5c7 License update (#199) 2025-01-02 22:58:05 -08:00
Andrew Pareles 825c40bfff license 2025-01-02 22:40:43 -08:00
Andrew Pareles a3d3309b9d Merge pull request #197 from voideditor/model-selection
Fix line indexing for CtrlK
2025-01-02 18:59:08 -08:00
Andrew Pareles 32bfe9a111 fix line indexing when streaming 2025-01-02 18:58:44 -08:00
Andrew Pareles a9adcad560 misc 2025-01-02 17:57:38 -08:00
Mathew Pareles 80f7511eee default settings instructions 2025-01-02 17:34:31 -08:00
Andrew Pareles 7fe8f5c47d ctrlK line numbers are right 2025-01-02 17:26:28 -08:00
Mathew Pareles b31882565b fix watchreact 2025-01-02 17:11:32 -08:00
Andrew Pareles 6aaf425419 Settings update + ctrlK stream runs (but is broken) 2025-01-02 17:10:38 -08:00
Andrew Pareles b0d21dfdb1 Merge pull request #196 from voideditor/model-selection
misc necessary updates
2025-01-02 16:41:33 -08:00
Andrew Pareles 2087948789 misc necessary updates 2025-01-02 16:40:55 -08:00
Andrew Pareles 6feb713682 Merge pull request #192 from jcommaret/main
Fix - bundle - ajv missing deps
2025-01-02 16:31:45 -08:00
Andrew Pareles 667dc8bd7e merge 2025-01-02 16:31:29 -08:00
Andrew Pareles bdb0c65346 Merge pull request #195 from voideditor/model-selection
Merge model selection branch (ctrlK, model selection, UI)
2025-01-02 16:29:10 -08:00
Andrew Pareles df14b80d8b not sure where this came from 2025-01-02 16:28:33 -08:00
Andrew Pareles 86decf8ed9 add (c) headers 2025-01-02 16:27:06 -08:00
Andrew Pareles bd9b9a9058 minor updates 2025-01-02 16:20:19 -08:00
Mathew Pareles 83c1f3430a fix assistant chat 2025-01-02 15:51:00 -08:00
Andrew Pareles 02de8937ac update copy 2025-01-02 01:28:19 -08:00
Mathew Pareles 78f3819c33 move extractCodeFromResult 2025-01-01 23:19:16 -08:00
Mathew Pareles b9cea4906e fix chat bubble direction 2025-01-01 23:08:32 -08:00
Mathew Pareles 4a4677b5e2 center button; fix single line button 2025-01-01 22:35:48 -08:00
Andrew Pareles 5dab1a6a77 update product.json 2025-01-01 22:05:57 -08:00
Mathew Pareles 9b01d5ba7c minor changes 2025-01-01 21:28:44 -08:00
Mathew Pareles dcb708758d fix input resize 2025-01-01 21:28:44 -08:00
Andrew Pareles dd4548466a minor chagnes 2025-01-01 20:01:10 -08:00
Andrew Pareles cda489e3e0 small fixes 2025-01-01 05:04:51 -08:00
Andrew Pareles 13594ef06c rework copy 2025-01-01 04:57:10 -08:00
Andrew Pareles c2b8df06eb desc 2025-01-01 04:47:41 -08:00
Andrew Pareles 33809ec48b finish adding css 2025-01-01 04:44:43 -08:00
Andrew Pareles b4820abc9e css 2025-01-01 04:31:44 -08:00
Andrew Pareles 0e79dec0d7 VOID ICONS 2024-12-31 22:03:38 -08:00
Andrew Pareles 659f01413f misc + track down icons to change 2024-12-31 20:27:31 -08:00
Andrew Pareles 110534df30 memo model dropdown + add ability to remove ctrlK zone 2024-12-31 16:18:18 -08:00
Andrew Pareles d3b61867f0 ctrlK now switches tabs + UI + remove async 2024-12-31 15:41:24 -08:00
Andrew Pareles 954e611fda Merge branch 'new-services' into model-selection 2024-12-31 13:56:07 -08:00
Andrew Pareles 64760aae13 comment 2024-12-31 13:54:08 -08:00
Andrew Pareles d0d1124543 delete old files 2024-12-31 13:47:41 -08:00
Andrew Pareles 90a3fd0a21 merge 2024-12-31 13:45:48 -08:00
Andrew Pareles 11ed2545c7 Merge remote-tracking branch 'origin/main' into new-services 2024-12-31 13:40:20 -08:00
Andrew Pareles 3d1df92f6a re-add deletion logic 2024-12-31 13:39:31 -08:00
Andrew Pareles 4ca16caa4e fix misc history issues with ctrlk - looks good!!! 2024-12-31 02:24:34 -08:00
Andrew Pareles 06b5cca59a history snapshot is only for this URI 2024-12-30 23:29:05 -08:00
Andrew Pareles e3c292eee7 progress towards ctrlK 2024-12-30 22:14:55 -08:00
Andrew Pareles 99c3786d06 diffarea resize logic 2024-12-30 19:37:52 -08:00
Andrew Pareles 6e40a92315 add FIM streaming + instructions 2024-12-30 15:20:55 -08:00
Andrew Pareles a9b2ff631d ctrlK progress... 2024-12-30 03:04:32 -08:00
Mathew Pareles 198adaaf18 selection ui 2024-12-30 04:57:43 -05:00
Mathew Pareles 2df7fa9ebc better model selection 2024-12-30 04:36:31 -05:00
Mathew Pareles 312942b004 ollama instructions 2024-12-30 03:39:17 -05:00
Mathew Pareles 0a444f6ff8 mark if model is detected locally 2024-12-30 01:45:02 -05:00
Mathew Pareles 0c10cb73f6 fix polling enable bug 2024-12-29 23:45:46 -05:00
Andrew Pareles ef8bba00b9 update ctrlk 2024-12-29 19:16:16 -08:00
Mathew Pareles 3a3bc24fef better polling state 2024-12-29 05:07:52 -05:00
Andrew Pareles ff7830e704 resize observer mostly works 2024-12-28 22:30:46 -05:00
Andrew Pareles 01a345a391 ctrlk double redundancy fix 2024-12-28 21:28:20 -05:00
Andrew Pareles 33909b3143 add ctrl+K 2024-12-28 20:03:47 -05:00
Jérôme Commaret 9543bf8eb3 Fix : Ajv missing deps at bundle 2024-12-28 22:19:16 +01:00
Andrew Pareles 9a9d0da91e remove errors 2024-12-27 23:56:24 -05:00
Jérôme Commaret d85a316536 Merge branch 'voideditor:main' into main 2024-12-27 13:59:49 +01:00
Andrew Pareles 1df044fb02 Update product.json 2024-12-25 22:23:01 -05:00
Mathew Pareles 076e0add44 prepare large merge 2024-12-25 19:21:16 -08:00
Andrew Pareles dfbd729fa4 nightly progress 2024-12-24 23:45:58 -05:00
Mathew Pareles 8a17c9246b language logic 2024-12-24 17:34:18 -08:00
mp 1afb39b155 minor changes 2024-12-24 02:22:45 -08:00
mp f08fd04af5 autodetect language 2024-12-23 16:21:37 -08:00
Andrew Pareles ef9d0daef4 minor chat styles 2024-12-23 15:37:06 -08:00
Andrew Pareles ffe5a41b01 fix rounding 2024-12-23 04:33:49 -08:00
Andrew Pareles 372beb7a2d rm temp 2024-12-23 03:30:05 -08:00
Andrew Pareles 406f29bb5a + 2024-12-23 03:29:49 -08:00
Andrew Pareles ee8b98ff74 minor changes 2024-12-23 03:00:19 -08:00
Andrew Pareles ffb0549c7c update zoneStyleService 2024-12-23 03:00:19 -08:00
Andrew Pareles 8f9c2f0630 misc 2024-12-23 03:00:19 -08:00
Andrew Pareles 84bfc26dce debugging 2024-12-23 03:00:10 -08:00
Andrew Pareles 80a87ff3c6 improvements but might still be buggy 2024-12-23 02:43:19 -08:00
mp 806ec939e5 ux for providers 2024-12-23 02:34:23 -08:00
Andrew Pareles 98a22d2eb6 minor changes 2024-12-23 00:16:37 -08:00
Andrew Pareles ffa31bc289 update zoneStyleService 2024-12-23 00:14:54 -08:00
Andrew Pareles a1ab8a5796 misc 2024-12-22 23:52:21 -08:00
mp 70c7d0261c correct bg 2024-12-22 21:41:37 -08:00
mp e7af8d9448 selection style 2024-12-22 21:28:09 -08:00
mp e1feaba74a warning icon 2024-12-22 02:12:24 -08:00
mp 8456f979ba warning icon 2024-12-22 02:07:29 -08:00
Andrew Pareles 6630d596f6 code box UI 2024-12-21 22:43:34 -08:00
Andrew Pareles a435a59a2e inlineDiffEdits add support for ctrl+K 2024-12-21 22:40:08 -08:00
mp 9cf4980004 selection 2024-12-21 22:37:08 -08:00
Mathew Pareles 6298be394d indentation 2024-12-21 21:13:21 -08:00
Andrew Pareles e647ce0054 fix + mount sidebar on init 2024-12-21 18:46:45 -08:00
Andrew Pareles 5fb30fed87 sidebar action 2024-12-21 18:29:15 -08:00
mp 7cc57bf086 input styles 2024-12-21 18:19:19 -08:00
Andrew Pareles 3fb6a4890d watermark 2024-12-21 17:51:15 -08:00
mp 97fd332c4e add code editor block 2024-12-21 17:29:02 -08:00
Andrew Pareles 81282e16b3 minor contrib update 2024-12-21 00:15:14 -08:00
Andrew Pareles 382275fa9f delete built-in extensions so the build works 2024-12-21 00:00:57 -08:00
Andrew Pareles 91f8321278 delete autoenable 2024-12-20 02:08:30 -08:00
Andrew Pareles 9b1f21d7c1 Merge pull request #185 from voideditor/model-selection
Daily UI improvements
2024-12-20 02:07:49 -08:00
Andrew Pareles 739ac2562b protected fix 2024-12-19 22:44:29 -08:00
Andrew Pareles 298423928e update styles and descs 2024-12-19 18:44:26 -08:00
mp 702d11ed3e Merge branch 'polishing-changes' into model-selection 2024-12-19 17:09:01 -08:00
Andrew Pareles 4513b732d6 all changes 2024-12-19 17:07:58 -08:00
Andrew Pareles 78e72f0c80 300 2024-12-19 17:00:16 -08:00
Andrew Pareles f457f006fb settings page styles 2024-12-19 04:05:31 -08:00
mp d6074e7ab5 ctrlk draft 2024-12-19 02:14:26 -08:00
Andrew Pareles 3669401819 upgrade to switches 2024-12-19 01:28:43 -08:00
Andrew Pareles ee7d0ed836 Merge pull request #181 from bjoaquinc/react-build-watcher
Fix React build crashing watch-client and added watch mode
2024-12-18 19:45:24 -08:00
Andrew Pareles ba87b4f0de update contrib 2024-12-18 19:40:27 -08:00
Andrew Pareles e4bc21ffb1 Merge pull request #182 from voideditor/model-selection
UI Improvements and Model Selection
2024-12-18 19:33:57 -08:00
Andrew Pareles 81b78b7765 auto fetch model works 2024-12-18 19:31:50 -08:00
Andrew Pareles c3c53b5e81 slightly fix refresh state 2024-12-18 16:48:04 -08:00
Andrew Pareles f49a6f8e41 minor style changes and refresh model works in general 2024-12-18 16:06:53 -08:00
mp 5a26aa8746 Merge branch 'model-selection' into polishing-changes 2024-12-18 15:45:18 -08:00
Andrew Pareles 8661fe1417 fix selection styles and add void watermark (buttons look bad) 2024-12-18 13:35:34 -08:00
Joaquin Coromina 2a519157da Merge remote-tracking branch 'origin/react-build-watcher' into recovered-code
Added a watcher that syncs react with watch-client without crashing#
2024-12-18 18:47:37 +08:00
Joaquin Coromina 610b52587b Added watchreact npm command, resolved watch-client crashes during updates, and included nodemon as a dependency 2024-12-18 18:24:17 +08:00
Jérôme Commaret 2d80a29275 Merge branch 'voideditor:main' into main 2024-12-12 01:03:51 +01:00
Jérôme Commaret d8e9b50890 Advanced builder section added 2024-12-07 14:49:28 +01:00
88 changed files with 9029 additions and 5799 deletions
+1
View File
@@ -21,3 +21,4 @@ vscode.db
product.overrides.json
*.snap.actual
.vscode-test
.tmp/
+21 -10
View File
@@ -8,7 +8,7 @@ There are a few ways to contribute:
- 💡 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.
Most of Void's code lives in `src/vs/workbench/contrib/void/browser/` and `src/vs/platform/void/`.
Void's code lives in `src/vs/workbench/contrib/void/browser/` and `src/vs/platform/void/`.
@@ -43,19 +43,19 @@ First, run `npm install -g node-gyp`. Then:
### Building Void
To build Void, open `void/` inside VSCode. Then:
To build Void, open `void/` inside VSCode. Then open your terminal and run:
1. `npm install` to install all dependencies.
2. `npm run buildreact` to build Void's browser dependencies like React.
3. Build.
2. `npm run watchreact` to build Void's browser dependencies like React.
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.
4. Run Void.
- 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+Shift+P</kbd> and run "Reload Window" inside the new window to see changes without re-building, unless they're React changes.
- 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.
#### Building Void from Terminal
@@ -74,16 +74,17 @@ Alternatively, if you want to build Void from the terminal, instead of pressing
- Make sure you follow the prerequisite steps.
- Make sure you have the same NodeJS version as `.nvmrc`.
- If you make any React changes, you must re-run `npm run buildreact` and re-build.
- 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.
## Bundling
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, run one of the following commands. This will create a folder named `VSCode-darwin-arm64` (or similar) in the repo's parent's directory. Be patient - compiling can take ~25 minutes.
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.
### Mac
- `npm run gulp vscode-darwin-arm64` - most common (Apple Silicon)
@@ -99,6 +100,16 @@ We don't usually recommend bundling. Instead, you should probably just build. If
- `npm run gulp vscode-linux-ia32`
### Output
This will generate a folder outside of `void/`:
```bash
workspace/
├── void/ # Your Void fork
└── VSCode-darwin-arm64/ # Generated output
```
# Guidelines
+26
View File
@@ -0,0 +1,26 @@
Void is a fork of VS Code, which is licensed under the MIT License (below).
Void's additions and modifications are licensed under the Apache 2.0 License (see LICENSE.txt).
--------------------
MIT License
Copyright (c) 2015 - present Microsoft Corporation
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:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
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.
+197 -17
View File
@@ -1,21 +1,201 @@
MIT License
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
Copyright (c) 2015 - present Microsoft Corporation
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 -1
View File
@@ -13,4 +13,4 @@ To build and run Void, follow the steps in [`CONTRIBUTING.md`](https://github.co
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).
## Support
Feel free to reach out in our [Discord](https://discord.gg/RSNjgaugJs) or contact us via email.
Feel free to reach out in our [Discord](https://discord.gg/RSNjgaugJs) server or contact us via email.
-41
View File
@@ -1,41 +0,0 @@
<!-- BEGIN MICROSOFT SECURITY.MD V0.0.9 BLOCK -->
## Security
Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet) and [Xamarin](https://github.com/xamarin).
If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/security.md/definition), please report it to us as described below.
## Reporting Security Issues
**Please do not report security vulnerabilities through public GitHub issues.**
Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/security.md/msrc/create-report).
If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/security.md/msrc/pgp).
You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc).
Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue:
* Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.)
* Full paths of source file(s) related to the manifestation of the issue
* The location of the affected source code (tag/branch/commit or direct URL)
* Any special configuration required to reproduce the issue
* Step-by-step instructions to reproduce the issue
* Proof-of-concept or exploit code (if possible)
* Impact of the issue, including how an attacker might exploit the issue
This information will help us triage your report more quickly.
If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/security.md/msrc/bounty) page for more details about our active programs.
## Preferred Languages
We prefer all communications to be in English.
## Policy
Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/security.md/cvd).
<!-- END MICROSOFT SECURITY.MD BLOCK -->
+1 -1
View File
@@ -32,6 +32,6 @@ Void is no longer an extension, so these links are no longer required, but they
- [The Full VSCode Extension API](https://code.visualstudio.com/api/references/vscode-api) - look on the right side for organization. The [bottom](https://code.visualstudio.com/api/references/vscode-api#api-patterns) of the page is easy to miss but is useful - cancellation tokens, events, disposables.
- [Activation events](https://code.visualstudio.com/api/references/activation-events) you can define in `package.json` (not the most useful)
- [Activation events](https://code.visualstudio.com/api/references/activation-events) you can define in `package.json` (not the most useful).
+4 -4
View File
@@ -425,15 +425,15 @@ function packageTask(platform, arch, sourceFolderName, destinationFolderName, op
'resources/win32/vue.ico',
'resources/win32/xml.ico',
'resources/win32/yaml.ico',
'resources/win32/code_70x70.png',
'resources/win32/code_150x150.png'
'resources/win32/code_70x70.png', // <-- Void icon
'resources/win32/code_150x150.png' // <-- Void icon
], { base: '.' }));
} else if (platform === 'linux') {
all = es.merge(all, gulp.src('resources/linux/code.png', { base: '.' }));
all = es.merge(all, gulp.src('resources/linux/code.png', { base: '.' })); // <-- Void icon
} else if (platform === 'darwin') {
const shortcut = gulp.src('resources/darwin/bin/code.sh')
.pipe(replace('@@APPNAME@@', product.applicationName))
.pipe(rename('bin/code'));
.pipe(rename('bin/code')); // <-- Void icon
all = es.merge(all, shortcut);
}
+2 -2
View File
@@ -54,7 +54,7 @@ function darwinBundleDocumentType(extensions, icon, nameOrSuffix, utis) {
role: 'Editor',
ostypes: ['TEXT', 'utxt', 'TUTX', '****'],
extensions,
iconFile: 'resources/darwin/' + icon.toLowerCase() + '.icns',
iconFile: 'resources/darwin/' + icon.toLowerCase() + '.icns', // <-- Void icon code.icns
utis
};
}
@@ -179,7 +179,7 @@ exports.config = {
darwinForceDarkModeSupport: true,
darwinCredits: darwinCreditsTemplate ? Buffer.from(darwinCreditsTemplate({ commit: commit, date: new Date().toISOString() })) : undefined,
linuxExecutableName: product.applicationName,
winIcon: 'resources/win32/code.ico',
winIcon: 'resources/win32/code.ico', // <-- Void icon
token: process.env['GITHUB_TOKEN'],
repo: product.electronRepository || undefined,
validateChecksum: true,
+2 -2
View File
@@ -68,7 +68,7 @@ function darwinBundleDocumentType(extensions: string[], icon: string, nameOrSuff
role: 'Editor',
ostypes: ['TEXT', 'utxt', 'TUTX', '****'],
extensions,
iconFile: 'resources/darwin/' + icon.toLowerCase() + '.icns',
iconFile: 'resources/darwin/' + icon.toLowerCase() + '.icns', // <-- Void icon code.icns
utis
};
}
@@ -196,7 +196,7 @@ export const config = {
darwinForceDarkModeSupport: true,
darwinCredits: darwinCreditsTemplate ? Buffer.from(darwinCreditsTemplate({ commit: commit, date: new Date().toISOString() })) : undefined,
linuxExecutableName: product.applicationName,
winIcon: 'resources/win32/code.ico',
winIcon: 'resources/win32/code.ico', // <-- Void icon
token: process.env['GITHUB_TOKEN'],
repo: product.electronRepository || undefined,
validateChecksum: true,
+5 -2
View File
@@ -4,6 +4,7 @@
? ('; LicenseFile: "' + RepoDir + '\licenses\LICENSE-' + Language + '.rtf"') \
: '; LicenseFile: "' + RepoDir + '\' + RootLicenseFileName + '"'
[Setup]
AppId={#AppId}
AppName={#NameLong}
@@ -20,8 +21,10 @@ Compression=lzma
SolidCompression=yes
AppMutex={code:GetAppMutex}
SetupMutex={#AppMutex}setup
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"
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"
; 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"
WizardSmallImageFile="{#RepoDir}\resources\win32\inno-void.bmp"
SetupIconFile={#RepoDir}\resources\win32\code.ico
UninstallDisplayIcon={app}\{#ExeBasename}.exe
ChangesEnvironment=true
+4164 -3846
View File
File diff suppressed because it is too large Load Diff
+9 -3
View File
@@ -1,15 +1,19 @@
{
"name": "code-oss-dev",
"name": "void-dev",
"productName": "Void",
"version": "1.94.0",
"distro": "ffcc24343ac46468a625666e5b9e673971dd1a1f",
"distro": "this is a commit number if we want to publish on npm",
"homepage": "https://voideditor.com",
"author": {
"name": "Microsoft Corporation"
"name": "Glass Devtools, Inc.",
"email": "andrew@voideditor.com"
},
"license": "MIT",
"main": "./out/main",
"private": true,
"scripts": {
"buildreact": "cd ./src/vs/workbench/contrib/void/browser/react/ && node build.js && cd ../../../../../../../",
"watchreact": "cd ./src/vs/workbench/contrib/void/browser/react/ && node build.js --watch && cd ../../../../../../../",
"test": "echo Please run any of the test scripts from the scripts folder.",
"test-browser": "npx playwright install && node test/unit/browser/index.js",
"test-browser-amd": "npx playwright install && node test/unit/browser/index.amd.js",
@@ -102,6 +106,7 @@
"@xterm/addon-webgl": "^0.19.0-beta.64",
"@xterm/headless": "^5.6.0-beta.64",
"@xterm/xterm": "^5.6.0-beta.64",
"ajv": "^8.17.1",
"diff": "^7.0.0",
"groq-sdk": "^0.9.0",
"http-proxy-agent": "^7.0.0",
@@ -218,6 +223,7 @@
"mocha": "^10.2.0",
"mocha-junit-reporter": "^2.2.1",
"mocha-multi-reporters": "^1.5.1",
"nodemon": "^3.1.9",
"npm-run-all": "^4.1.5",
"opn": "^6.0.0",
"original-fs": "^1.2.0",
+24 -74
View File
@@ -1,88 +1,38 @@
{
"nameShort": "Void",
"nameLong": "Void",
"applicationName": "code-oss",
"dataFolderName": ".vscode-oss",
"win32MutexName": "vscodeoss",
"applicationName": "void",
"dataFolderName": ".void-editor",
"win32MutexName": "voideditor",
"licenseName": "MIT",
"licenseUrl": "https://github.com/microsoft/vscode/blob/main/LICENSE.txt",
"serverLicenseUrl": "https://github.com/microsoft/vscode/blob/main/LICENSE.txt",
"licenseUrl": "https://github.com/voideditor/void/blob/main/LICENSE.txt",
"serverLicenseUrl": "https://github.com/voideditor/void/blob/main/LICENSE.txt",
"serverGreeting": [],
"serverLicense": [],
"serverLicensePrompt": "",
"serverApplicationName": "code-server-oss",
"serverDataFolderName": ".vscode-server-oss",
"tunnelApplicationName": "code-tunnel-oss",
"win32DirName": "Microsoft Code OSS",
"win32NameVersion": "Microsoft Code OSS",
"win32RegValueName": "CodeOSS",
"win32x64AppId": "{{D77B7E06-80BA-4137-BCF4-654B95CCEBC5}",
"win32arm64AppId": "{{D1ACE434-89C5-48D1-88D3-E2991DF85475}",
"win32x64UserAppId": "{{CC6B787D-37A0-49E8-AE24-8559A032BE0C}",
"win32arm64UserAppId": "{{3AEBF0C8-F733-4AD4-BADE-FDB816D53D7B}",
"win32AppUserModelId": "Microsoft.CodeOSS",
"win32ShellNameShort": "C&ode - OSS",
"win32TunnelServiceMutex": "vscodeoss-tunnelservice",
"win32TunnelMutex": "vscodeoss-tunnel",
"darwinBundleIdentifier": "com.visualstudio.code.oss",
"linuxIconName": "code-oss",
"serverApplicationName": "void-server",
"serverDataFolderName": ".void-server",
"tunnelApplicationName": "void-tunnel",
"win32DirName": "Void",
"win32NameVersion": "Void",
"win32RegValueName": "VoidEditor",
"win32x64AppId": "{{9D394D01-1728-45A7-B997-A6C82C5452C3}",
"win32arm64AppId": "{{0668DD58-2BDE-4101-8CDA-40252DF8875D}",
"win32x64UserAppId": "{{8BED5DC1-6C55-46E6-9FE6-18F7E6F7C7F1}",
"win32arm64UserAppId": "{{F6C87466-BC82-4A8F-B0FF-18CA366BA4D8}",
"win32AppUserModelId": "Void.Editor",
"win32ShellNameShort": "V&oid",
"win32TunnelServiceMutex": "void-tunnelservice",
"win32TunnelMutex": "void-tunnel",
"darwinBundleIdentifier": "com.voideditor.code",
"linuxIconName": "void-editor",
"licenseFileName": "LICENSE.txt",
"reportIssueUrl": "https://github.com/microsoft/vscode/issues/new",
"reportIssueUrl": "https://github.com/voideditor/void/issues/new",
"nodejsRepository": "https://nodejs.org",
"urlProtocol": "code-oss",
"webviewContentExternalBaseUrlTemplate": "https://{{uuid}}.vscode-cdn.net/insider/ef65ac1ba57f57f2a3961bfe94aa20481caca4c6/out/vs/workbench/contrib/webview/browser/pre/",
"urlProtocol": "void-editor",
"extensionsGallery": {
"serviceUrl": "https://open-vsx.org/vscode/gallery",
"itemUrl": "https://open-vsx.org/vscode/item"
},
"builtInExtensions": [
{
"name": "ms-vscode.js-debug-companion",
"version": "1.1.3",
"sha256": "7380a890787452f14b2db7835dfa94de538caf358ebc263f9d46dd68ac52de93",
"repo": "https://github.com/microsoft/vscode-js-debug-companion",
"metadata": {
"id": "99cb0b7f-7354-4278-b8da-6cc79972169d",
"publisherId": {
"publisherId": "5f5636e7-69ed-4afe-b5d6-8d231fb3d3ee",
"publisherName": "ms-vscode",
"displayName": "Microsoft",
"flags": "verified"
},
"publisherDisplayName": "Microsoft"
}
},
{
"name": "ms-vscode.js-debug",
"version": "1.93.0",
"sha256": "9339cb8e6b77f554df54d79e71f533279cb76b0f9b04c207f633bfd507442b6a",
"repo": "https://github.com/microsoft/vscode-js-debug",
"metadata": {
"id": "25629058-ddac-4e17-abba-74678e126c5d",
"publisherId": {
"publisherId": "5f5636e7-69ed-4afe-b5d6-8d231fb3d3ee",
"publisherName": "ms-vscode",
"displayName": "Microsoft",
"flags": "verified"
},
"publisherDisplayName": "Microsoft"
}
},
{
"name": "ms-vscode.vscode-js-profile-table",
"version": "1.0.9",
"sha256": "3b62ee4276a2bbea3fe230f94b1d5edd915b05966090ea56f882e1e0ab53e1a6",
"repo": "https://github.com/microsoft/vscode-js-profile-visualizer",
"metadata": {
"id": "7e52b41b-71ad-457b-ab7e-0620f1fc4feb",
"publisherId": {
"publisherId": "5f5636e7-69ed-4afe-b5d6-8d231fb3d3ee",
"publisherName": "ms-vscode",
"displayName": "Microsoft",
"flags": "verified"
},
"publisherDisplayName": "Microsoft"
}
}
]
"builtInExtensions": []
}
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 813 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 395 B

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 338 B

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 MiB

+1 -1
View File
@@ -159,7 +159,7 @@ export class InputBox extends Widget {
this.scrollableElement = new ScrollableElement(this.element, { vertical: ScrollbarVisibility.Auto });
if (this.options.flexibleWidth) {
this.input.setAttribute('wrap', 'off');
this.input.setAttribute('wrap', 'on');
this.mirror.style.whiteSpace = 'pre';
this.mirror.style.wordWrap = 'initial';
}
@@ -6,6 +6,11 @@ import { ICodeEditor, IViewZone } from '../../editorBrowser.js';
import { IRange } from '../../../common/core/range.js';
import { EditorOption } from '../../../common/config/editorOptions.js';
// THIS FILE IS OLD + UNUSED!!!
// SEE inlineDiffsService.ts INSTEAD.
export interface IInlineDiffService {
readonly _serviceBrand: undefined;
addDiff(editor: ICodeEditor, originalText: string, modifiedRange: IRange): void;
@@ -22,7 +22,7 @@ export class ExpandLineSelectionAction extends EditorAction {
kbOpts: {
weight: KeybindingWeight.EditorCore,
kbExpr: EditorContextKeys.textInputFocus,
primary: KeyMod.CtrlCmd | KeyCode.KeyL
primary: KeyMod.CtrlCmd | KeyCode.KeyM // Void changed this to Cmd+M
},
});
}
@@ -1,4 +1,7 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
// ---------- common ----------
@@ -1,9 +1,9 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
/*--------------------------------------------------------------------------------------
* 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, ServiceOllamaListParams, EventOllamaListOnSuccessParams, EventOllamaListOnErrorParams, MainOllamaListParams } from './llmMessageTypes.js';
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';
import { IMainProcessService } from '../../ipc/common/mainProcessService.js';
import { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js';
@@ -21,7 +21,8 @@ export interface ILLMMessageService {
readonly _serviceBrand: undefined;
sendLLMMessage: (params: ServiceSendLLMMessageParams) => string | null;
abort: (requestId: string) => void;
ollamaList: (params: ServiceOllamaListParams) => void;
ollamaList: (params: ServiceModelListParams<OllamaModelResponse>) => void;
openAICompatibleList: (params: ServiceModelListParams<OpenaiCompatibleModelResponse>) => void;
}
export class LLMMessageService extends Disposable implements ILLMMessageService {
@@ -36,9 +37,12 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
// ollamaList
private readonly onSuccess_ollama: { [eventId: string]: ((params: EventOllamaListOnSuccessParams) => void) } = {}
private readonly onError_ollama: { [eventId: string]: ((params: EventOllamaListOnErrorParams) => void) } = {}
private readonly onSuccess_ollama: { [eventId: string]: ((params: EventModelListOnSuccessParams<OllamaModelResponse>) => void) } = {}
private readonly onError_ollama: { [eventId: string]: ((params: EventModelListOnErrorParams<OllamaModelResponse>) => void) } = {}
// openAICompatibleList
private readonly onSuccess_openAICompatible: { [eventId: string]: ((params: EventModelListOnSuccessParams<OpenaiCompatibleModelResponse>) => void) } = {}
private readonly onError_openAICompatible: { [eventId: string]: ((params: EventModelListOnErrorParams<OpenaiCompatibleModelResponse>) => void) } = {}
constructor(
@IMainProcessService private readonly mainProcessService: IMainProcessService, // used as a renderer (only usable on client side)
@@ -65,12 +69,19 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
this._onRequestIdDone(e.requestId)
}))
// ollama
this._register((this.channel.listen('onSuccess_ollama') satisfies Event<EventOllamaListOnSuccessParams>)(e => {
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<EventOllamaListOnErrorParams>)(e => {
this._register((this.channel.listen('onError_ollama') satisfies Event<EventModelListOnErrorParams<OllamaModelResponse>>)(e => {
this.onError_ollama[e.requestId]?.(e)
}))
// openaiCompatible
this._register((this.channel.listen('onSuccess_openAICompatible') satisfies Event<EventModelListOnSuccessParams<OpenaiCompatibleModelResponse>>)(e => {
this.onSuccess_openAICompatible[e.requestId]?.(e)
}))
this._register((this.channel.listen('onError_openAICompatible') satisfies Event<EventModelListOnErrorParams<OpenaiCompatibleModelResponse>>)(e => {
this.onError_openAICompatible[e.requestId]?.(e)
}))
}
@@ -113,7 +124,7 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
}
ollamaList = (params: ServiceOllamaListParams) => {
ollamaList = (params: ServiceModelListParams<OllamaModelResponse>) => {
const { onSuccess, onError, ...proxyParams } = params
const { settingsOfProvider } = this.voidSettingsService.state
@@ -127,7 +138,24 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
...proxyParams,
settingsOfProvider,
requestId: requestId_,
} satisfies MainOllamaListParams)
} satisfies MainModelListParams<OllamaModelResponse>)
}
openAICompatibleList = (params: ServiceModelListParams<OpenaiCompatibleModelResponse>) => {
const { onSuccess, onError, ...proxyParams } = params
const { settingsOfProvider } = this.voidSettingsService.state
// add state for request id
const requestId_ = generateUuid();
this.onSuccess_openAICompatible[requestId_] = onSuccess
this.onError_openAICompatible[requestId_] = onError
this.channel.call('openAICompatibleList', {
...proxyParams,
settingsOfProvider,
requestId: requestId_,
} satisfies MainModelListParams<OpenaiCompatibleModelResponse>)
}
+32 -23
View File
@@ -1,7 +1,7 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
/*--------------------------------------------------------------------------------------
* 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'
@@ -17,14 +17,14 @@ export type LLMMessage = {
content: string;
}
export type LLMFeatureSelection = {
featureName: 'Ctrl+K',
range: IRange
export type ServiceSendLLMFeatureParams = {
featureName: 'Ctrl+K';
range: IRange;
} | {
featureName: 'Ctrl+L',
featureName: 'Ctrl+L';
} | {
featureName: 'Autocomplete',
range: IRange
featureName: 'Autocomplete';
range: IRange;
}
// params to the true sendLLMMessage function
@@ -54,7 +54,7 @@ export type ServiceSendLLMMessageParams = {
logging: {
loggingName: string,
};
} & LLMFeatureSelection
} & ServiceSendLLMFeatureParams
// can't send functions across a proxy, use listeners instead
export type BlockedMainLLMMessageParams = 'onText' | 'onFinalMessage' | 'onError' | 'abortRef'
@@ -97,7 +97,7 @@ export type _InternalSendLLMMessageFnType = (params: {
// These are from 'ollama' SDK
interface ModelDetails {
interface OllamaModelDetails {
parent_model: string;
format: string;
family: string;
@@ -106,35 +106,44 @@ interface ModelDetails {
quantization_level: string;
}
export type ModelResponse = {
export type OllamaModelResponse = {
name: string;
modified_at: Date;
size: number;
digest: string;
details: ModelDetails;
details: OllamaModelDetails;
expires_at: Date;
size_vram: number;
}
export type OpenaiCompatibleModelResponse = {
id: string;
created: number;
object: 'model';
owned_by: string;
}
// params to the true list fn
export type OllamaListParams = {
export type ModelListParams<modelResponse> = {
settingsOfProvider: SettingsOfProvider;
onSuccess: (param: { models: ModelResponse[] }) => void;
onSuccess: (param: { models: modelResponse[] }) => void;
onError: (param: { error: string }) => void;
}
export type ServiceOllamaListParams = {
onSuccess: (param: { models: ModelResponse[] }) => void;
// params to the service
export type ServiceModelListParams<modelResponse> = {
onSuccess: (param: { models: modelResponse[] }) => void;
onError: (param: { error: any }) => void;
}
type BlockedMainOllamaListParams = 'onSuccess' | 'onError'
export type MainOllamaListParams = Omit<OllamaListParams, BlockedMainOllamaListParams> & { requestId: string }
type BlockedMainModelListParams = 'onSuccess' | 'onError'
export type MainModelListParams<modelResponse> = Omit<ModelListParams<modelResponse>, BlockedMainModelListParams> & { requestId: string }
export type EventOllamaListOnSuccessParams = Parameters<OllamaListParams['onSuccess']>[0] & { requestId: string }
export type EventOllamaListOnErrorParams = Parameters<OllamaListParams['onError']>[0] & { requestId: string }
export type EventModelListOnSuccessParams<modelResponse> = Parameters<ModelListParams<modelResponse>['onSuccess']>[0] & { requestId: string }
export type EventModelListOnErrorParams<modelResponse> = Parameters<ModelListParams<modelResponse>['onError']>[0] & { requestId: string }
export type _InternalOllamaListFnType = (params: OllamaListParams) => void
export type _InternalModelListFnType<modelResponse> = (params: ModelListParams<modelResponse>) => void
@@ -1,7 +1,7 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
/*--------------------------------------------------------------------------------------
* 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';
@@ -1,17 +1,45 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
/*--------------------------------------------------------------------------------------
* 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';
import { IVoidSettingsService } from './voidSettingsService.js';
import { ILLMMessageService } from './llmMessageService.js';
import { Emitter, Event } from '../../../base/common/event.js';
import { Disposable } from '../../../base/common/lifecycle.js';
import { Disposable, IDisposable } from '../../../base/common/lifecycle.js';
import { RefreshableProviderName, refreshableProviderNames, SettingsOfProvider } from './voidSettingsTypes.js';
import { OllamaModelResponse, OpenaiCompatibleModelResponse } from './llmMessageTypes.js';
export type RefreshModelState = 'done' | 'loading'
type RefreshableState = ({
state: 'init',
timeoutId: null,
} | {
state: 'refreshing',
timeoutId: NodeJS.Timeout | null, // the timeoutId of the most recent call to refreshModels
} | {
state: 'finished',
timeoutId: null,
} | {
state: 'finished_invisible',
timeoutId: null,
})
export type RefreshModelStateOfProvider = Record<RefreshableProviderName, RefreshableState>
const refreshBasedOn: { [k in RefreshableProviderName]: (keyof SettingsOfProvider[k])[] } = {
ollama: ['_enabled', 'endpoint'],
openAICompatible: ['_enabled', 'endpoint', 'apiKey'],
}
const REFRESH_INTERVAL = 5_000
// const COOLDOWN_TIMEOUT = 300
// element-wise equals
function eq<T>(a: T[], b: T[]): boolean {
@@ -23,9 +51,9 @@ function eq<T>(a: T[], b: T[]): boolean {
}
export interface IRefreshModelService {
readonly _serviceBrand: undefined;
refreshOllamaModels(): void;
onDidChangeState: Event<void>;
state: RefreshModelState;
refreshModels: (providerName: RefreshableProviderName) => Promise<void>;
onDidChangeState: Event<RefreshableProviderName>;
state: RefreshModelStateOfProvider;
}
export const IRefreshModelService = createDecorator<IRefreshModelService>('RefreshModelService');
@@ -34,8 +62,9 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
readonly _serviceBrand: undefined;
private readonly _onDidChangeState = new Emitter<void>();
readonly onDidChangeState: Event<void> = this._onDidChangeState.event; // this is primarily for use in react, so react can listen + update on state changes
private readonly _onDidChangeState = new Emitter<RefreshableProviderName>();
readonly onDidChangeState: Event<RefreshableProviderName> = this._onDidChangeState.event; // this is primarily for use in react, so react can listen + update on state changes
constructor(
@IVoidSettingsService private readonly voidSettingsService: IVoidSettingsService,
@@ -43,62 +72,134 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
) {
super()
// on mount, refresh ollama models
this.refreshOllamaModels()
// every time ollama.enabled changes, refresh ollama models, like useEffect
let relevantVals = () => [this.voidSettingsService.state.settingsOfProvider.ollama.enabled, this.voidSettingsService.state.settingsOfProvider.ollama.endpoint]
let prevVals = relevantVals()
this._register(
this.voidSettingsService.onDidChangeState(() => { // we might want to debounce this
const newVals = relevantVals()
if (!eq(prevVals, newVals)) {
this.refreshOllamaModels()
prevVals = newVals
}
})
)
const disposables: Set<IDisposable> = new Set()
const initializePollingAndOnChange = () => {
this._clearAllTimeouts()
disposables.forEach(d => d.dispose())
disposables.clear()
if (!voidSettingsService.state.featureFlagSettings.autoRefreshModels) return
for (const providerName of refreshableProviderNames) {
const { _enabled: enabled } = this.voidSettingsService.state.settingsOfProvider[providerName]
this.refreshModels(providerName, !enabled, { isPolling: true, isInternal: true })
// every time providerName.enabled changes, refresh models too, like a useEffect
let relevantVals = () => refreshBasedOn[providerName].map(settingName => this.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
const newVals = relevantVals()
if (!eq(prevVals, newVals)) {
const prevEnabled = prevVals[0] as boolean
const enabled = newVals[0] as boolean
// 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 })
}
else {
// else if user just clicked disable, don't refresh
// //give cooldown before re-enabling (or at least re-fetching)
// const timeoutId = setTimeout(() => this.refreshModels(providerName, !enabled), COOLDOWN_TIMEOUT)
// this._setTimeoutId(providerName, timeoutId)
}
prevVals = newVals
}
})
)
}
}
// 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
voidSettingsService.waitForInitState.then(() => {
initializePollingAndOnChange()
this._register(
voidSettingsService.onDidChangeState((type) => { if (type === 'featureFlagSettings') initializePollingAndOnChange() })
)
})
}
state: RefreshModelState = 'done'
private _timeoutId: NodeJS.Timeout | null = null
private _cancelTimeout = () => {
if (this._timeoutId) {
clearTimeout(this._timeoutId)
this._timeoutId = null
}
state: RefreshModelStateOfProvider = {
ollama: { state: 'init', timeoutId: null },
openAICompatible: { state: 'init', timeoutId: null },
}
async refreshOllamaModels() {
// cancel any existing poll
this._cancelTimeout()
// if ollama is disabled, obivously done
if (!this.voidSettingsService.state.settingsOfProvider.ollama.enabled) {
this._setState('done')
return
}
// 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}`)
this._clearProviderTimeout(providerName)
// start loading models
this._setState('loading')
if (!isInternal) this._setRefreshState(providerName, 'refreshing')
this.llmMessageService.ollamaList({
const fn = providerName === 'ollama' ? this.llmMessageService.ollamaList
: providerName === 'openAICompatible' ? this.llmMessageService.openAICompatibleList
: () => { }
fn({
onSuccess: ({ models }) => {
this.voidSettingsService.setDefaultModels('ollama', models.map(model => model.name))
this._setState('done')
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)
}
if (!isInternal) this._setRefreshState(providerName, 'finished')
},
onError: ({ error }) => {
// poll
console.log('retrying ollamaList:', error)
this._timeoutId = setTimeout(() => this.refreshOllamaModels(), 5000)
console.log('retrying list models:', providerName, error)
}
})
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)
}
}
private _setState(state: RefreshModelState) {
this.state = state
this._onDidChangeState.fire()
_clearAllTimeouts() {
for (const providerName of refreshableProviderNames) {
this._clearProviderTimeout(providerName)
}
}
_clearProviderTimeout(providerName: RefreshableProviderName) {
// cancel any existing poll
if (this.state[providerName].timeoutId) {
clearTimeout(this.state[providerName].timeoutId)
this._setTimeoutId(providerName, null)
}
}
private _setTimeoutId(providerName: RefreshableProviderName, timeoutId: NodeJS.Timeout | null) {
this.state[providerName].timeoutId = timeoutId
}
private _setRefreshState(providerName: RefreshableProviderName, state: RefreshableState['state']) {
this.state[providerName].state = state
this._onDidChangeState.fire(providerName)
}
}
@@ -1,7 +1,7 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
/*--------------------------------------------------------------------------------------
* 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,10 @@ 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, ModelInfo } from './voidSettingsTypes.js';
import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, modelInfoOfDefaultNames, VoidModelInfo, FeatureFlagSettings, FeatureFlagName, defaultFeatureFlagSettings } from './voidSettingsTypes.js';
const STORAGE_KEY = 'void.voidSettingsI'
const STORAGE_KEY = 'void.voidSettingsStorage'
type SetSettingOfProviderFn = <S extends SettingName>(
providerName: ProviderName,
@@ -21,13 +21,13 @@ type SetSettingOfProviderFn = <S extends SettingName>(
newVal: SettingsOfProvider[ProviderName][S extends keyof SettingsOfProvider[ProviderName] ? S : never],
) => Promise<void>;
type SetModelSelectionOfFeature = <K extends FeatureName>(
type SetModelSelectionOfFeatureFn = <K extends FeatureName>(
featureName: K,
newVal: ModelSelectionOfFeature[K],
options?: { doNotApplyEffects?: true }
) => Promise<void>;
type SetFeatureFlagFn = (flagName: FeatureFlagName, newVal: boolean) => void;
export type ModelOption = { text: string, value: ModelSelection }
@@ -36,18 +36,24 @@ export type ModelOption = { text: string, value: ModelSelection }
export type VoidSettingsState = {
readonly settingsOfProvider: SettingsOfProvider; // optionsOfProvider
readonly modelSelectionOfFeature: ModelSelectionOfFeature; // stateOfFeature
readonly featureFlagSettings: FeatureFlagSettings;
readonly _modelOptions: ModelOption[] // computed based on the two above items
}
type EventProp = Exclude<keyof VoidSettingsState, '_modelOptions'> | 'all'
export interface IVoidSettingsService {
readonly _serviceBrand: undefined;
readonly state: VoidSettingsState;
onDidChangeState: Event<void>;
readonly state: VoidSettingsState; // in order to play nicely with react, you should immutably change state
readonly waitForInitState: Promise<void>;
onDidChangeState: Event<EventProp>;
setSettingOfProvider: SetSettingOfProviderFn;
setModelSelectionOfFeature: SetModelSelectionOfFeature;
setModelSelectionOfFeature: SetModelSelectionOfFeatureFn;
setFeatureFlag: SetFeatureFlagFn;
setDefaultModels(providerName: ProviderName, modelNames: string[]): void;
toggleModelHidden(providerName: ProviderName, modelName: string): void;
@@ -60,7 +66,7 @@ let _computeModelOptions = (settingsOfProvider: SettingsOfProvider) => {
let modelOptions: ModelOption[] = []
for (const providerName of providerNames) {
const providerConfig = settingsOfProvider[providerName]
if (!providerConfig.enabled) continue // if disabled, don't display model options
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 } })
@@ -74,6 +80,7 @@ const defaultState = () => {
const d: VoidSettingsState = {
settingsOfProvider: deepClone(defaultSettingsOfProvider),
modelSelectionOfFeature: { 'Ctrl+L': null, 'Ctrl+K': null, 'Autocomplete': null },
featureFlagSettings: deepClone(defaultFeatureFlagSettings),
_modelOptions: _computeModelOptions(defaultSettingsOfProvider), // computed
}
return d
@@ -84,10 +91,11 @@ export const IVoidSettingsService = createDecorator<IVoidSettingsService>('VoidS
class VoidSettingsService extends Disposable implements IVoidSettingsService {
_serviceBrand: undefined;
private readonly _onDidChangeState = new Emitter<void>();
readonly onDidChangeState: Event<void> = this._onDidChangeState.event; // this is primarily for use in react, so react can listen + update on state changes
private readonly _onDidChangeState = new Emitter<EventProp>();
readonly onDidChangeState: Event<EventProp> = this._onDidChangeState.event; // this is primarily for use in react, so react can listen + update on state changes
state: VoidSettingsState;
waitForInitState: Promise<void> // await this if you need a valid state initially
constructor(
@IStorageService private readonly _storageService: IStorageService,
@@ -100,10 +108,14 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
// at the start, we haven't read the partial config yet, but we need to set state to something
this.state = defaultState()
let resolver: () => void = () => { }
this.waitForInitState = new Promise((res, rej) => resolver = res)
// read and update the actual state immediately
this._readState().then(s => {
this.state = s
this._onDidChangeState.fire()
resolver()
this._onDidChangeState.fire('all')
})
}
@@ -136,13 +148,16 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
}
}
const newFeatureFlags = this.state.featureFlagSettings
// if changed models or enabled a provider, recompute models list
const modelsListChanged = settingName === 'models' || settingName === 'enabled'
const modelsListChanged = settingName === 'models' || settingName === '_enabled'
const newModelsList = modelsListChanged ? _computeModelOptions(newSettingsOfProvider) : this.state._modelOptions
const newState: VoidSettingsState = {
modelSelectionOfFeature: newModelSelectionOfFeature,
settingsOfProvider: newSettingsOfProvider,
featureFlagSettings: newFeatureFlags,
_modelOptions: newModelsList,
}
@@ -166,11 +181,26 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
}
await this._storeState()
this._onDidChangeState.fire()
this._onDidChangeState.fire('settingsOfProvider')
}
setModelSelectionOfFeature: SetModelSelectionOfFeature = async (featureName, newVal, options) => {
setFeatureFlag: SetFeatureFlagFn = async (flagName, newVal) => {
const newState = {
...this.state,
featureFlagSettings: {
...this.state.featureFlagSettings,
[flagName]: newVal
}
}
this.state = newState
await this._storeState()
this._onDidChangeState.fire('featureFlagSettings')
}
setModelSelectionOfFeature: SetModelSelectionOfFeatureFn = async (featureName, newVal, options) => {
const newState: VoidSettingsState = {
...this.state,
modelSelectionOfFeature: {
@@ -185,14 +215,14 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
return
await this._storeState()
this._onDidChangeState.fire()
this._onDidChangeState.fire('modelSelectionOfFeature')
}
setDefaultModels(providerName: ProviderName, newDefaultModelNames: string[]) {
const { models } = this.state.settingsOfProvider[providerName]
const newDefaultModels = modelInfoOfDefaultNames(newDefaultModelNames)
const newDefaultModels = modelInfoOfDefaultNames(newDefaultModelNames, { isAutodetected: true, existingModels: models })
const newModels = [
...newDefaultModels,
...models.filter(m => !m.isDefault), // keep any non-default models
@@ -203,7 +233,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
const { models } = this.state.settingsOfProvider[providerName]
const modelIdx = models.findIndex(m => m.modelName === modelName)
if (modelIdx === -1) return
const newModels: ModelInfo[] = [
const newModels: VoidModelInfo[] = [
...models.slice(0, modelIdx),
{ ...models[modelIdx], isHidden: !models[modelIdx].isHidden },
...models.slice(modelIdx + 1, Infinity)
+155 -49
View File
@@ -1,22 +1,42 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
export type ModelInfo = {
export type VoidModelInfo = {
modelName: string,
isDefault: boolean, // whether or not it's a default for its provider
isHidden: boolean, // whether or not the user is hiding it
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[] => {
export const modelInfoOfDefaultNames = (modelNames: string[]): ModelInfo[] => {
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
return modelNames.map((modelName, i) => ({ modelName, isDefault: true, isHidden }))
if (!existingModels) {
return modelNames.map((modelName, i) => ({ modelName, isDefault, isAutodetected, isHidden, }))
} else {
// keep existing `isHidden` property
const existingModelsMap: Record<string, VoidModelInfo> = {}
for (const em of existingModels) {
existingModelsMap[em.modelName] = em
}
return modelNames.map((modelName, i) => ({ modelName, isDefault, isAutodetected, isHidden: !!existingModelsMap[modelName]?.isHidden, }))
}
}
// https://docs.anthropic.com/en/docs/about-claude/models
@@ -96,7 +116,7 @@ type UnionOfKeys<T> = T extends T ? keyof T : never;
export const customProviderSettingsDefaults = {
export const defaultProviderSettings = {
anthropic: {
apiKey: '',
},
@@ -110,8 +130,8 @@ export const customProviderSettingsDefaults = {
apiKey: '',
},
openAICompatible: {
apiKey: '',
endpoint: '',
apiKey: '',
},
gemini: {
apiKey: '',
@@ -121,22 +141,27 @@ export const customProviderSettingsDefaults = {
}
} as const
export type ProviderName = keyof typeof customProviderSettingsDefaults
export const providerNames = Object.keys(customProviderSettingsDefaults) as ProviderName[]
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 nonlocalProviderNames = providerNames.filter((name) => !(localProviderNames as string[]).includes(name)) // all non-local names
type CustomSettingName = UnionOfKeys<typeof customProviderSettingsDefaults[ProviderName]>
type CustomSettingName = UnionOfKeys<typeof defaultProviderSettings[ProviderName]>
type CustomProviderSettings<providerName extends ProviderName> = {
[k in CustomSettingName]: k extends keyof typeof customProviderSettingsDefaults[providerName] ? string : undefined
[k in CustomSettingName]: k extends keyof typeof defaultProviderSettings[providerName] ? string : undefined
}
export const customSettingNamesOfProvider = (providerName: ProviderName) => {
return Object.keys(defaultProviderSettings[providerName]) as CustomSettingName[]
}
type CommonProviderSettings = {
enabled: boolean,
models: ModelInfo[], // if null, user can type in any string as a model
_enabled: boolean | undefined, // undefined initially, computed when user types in all fields
models: VoidModelInfo[],
}
type SettingsForProvider<providerName extends ProviderName> = CustomProviderSettings<providerName> & CommonProviderSettings
export type SettingsForProvider<providerName extends ProviderName> = CustomProviderSettings<providerName> & CommonProviderSettings
// part of state
export type SettingsOfProvider = {
@@ -148,21 +173,49 @@ export type SettingName = keyof SettingsForProvider<ProviderName>
export const titleOfProviderName = (providerName: ProviderName) => {
if (providerName === 'anthropic')
return 'Anthropic'
else if (providerName === 'openAI')
return 'OpenAI'
else if (providerName === 'ollama')
return 'Ollama'
else if (providerName === 'openRouter')
return 'OpenRouter'
else if (providerName === 'openAICompatible')
return 'OpenAI-Compatible'
else if (providerName === 'gemini')
return 'Gemini'
else if (providerName === 'groq')
return 'Groq'
type DisplayInfoForProviderName = {
title: string,
}
export const displayInfoOfProviderName = (providerName: ProviderName): DisplayInfoForProviderName => {
if (providerName === 'anthropic') {
return {
title: 'Anthropic',
}
}
else if (providerName === 'openAI') {
return {
title: 'OpenAI',
}
}
else if (providerName === 'openRouter') {
return {
title: 'OpenRouter',
}
}
else if (providerName === 'ollama') {
return {
title: 'Ollama',
}
}
else if (providerName === 'openAICompatible') {
return {
title: 'Other',
}
}
else if (providerName === 'gemini') {
return {
title: 'Gemini',
}
}
else if (providerName === 'groq') {
return {
title: 'Groq',
}
}
throw new Error(`descOfProviderName: Unknown provider name: "${providerName}"`)
}
@@ -170,6 +223,7 @@ export const titleOfProviderName = (providerName: ProviderName) => {
type DisplayInfo = {
title: string,
placeholder: string,
subTextMd?: string,
}
export const displayInfoOfSettingName = (providerName: ProviderName, settingName: SettingName): DisplayInfo => {
if (settingName === 'apiKey') {
@@ -182,19 +236,31 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
providerName === 'groq' ? 'gsk_key...' :
providerName === 'openAICompatible' ? 'sk-key...' :
'(never)',
subTextMd: providerName === 'anthropic' ? 'Get your [API Key here](https://console.anthropic.com/settings/keys).' :
providerName === 'openAI' ? 'Get your [API Key here](https://platform.openai.com/api-keys).' :
providerName === 'openRouter' ? 'Get your [API Key here](https://openrouter.ai/settings/keys).' :
providerName === 'gemini' ? 'Get your [API Key here](https://aistudio.google.com/apikey).' :
providerName === 'groq' ? 'Get your [API Key here](https://console.groq.com/keys).' :
providerName === 'openAICompatible' ? 'Add any OpenAI-Compatible endpoint.' :
undefined,
}
}
else if (settingName === 'endpoint') {
return {
title: providerName === 'ollama' ? 'Your Ollama endpoint' :
title: providerName === 'ollama' ? 'Endpoint' :
providerName === 'openAICompatible' ? 'baseURL' // (do not include /chat/completions)
: '(never)',
placeholder: providerName === 'ollama' ? customProviderSettingsDefaults.ollama.endpoint
placeholder: providerName === 'ollama' ? defaultProviderSettings.ollama.endpoint
: 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).' :
undefined,
}
}
else if (settingName === 'enabled') {
else if (settingName === '_enabled') {
return {
title: '(never)',
placeholder: '(never)',
@@ -247,46 +313,46 @@ export const voidInitModelOptions = {
// used when waiting and for a type reference
export const defaultSettingsOfProvider: SettingsOfProvider = {
anthropic: {
_enabled: undefined,
...defaultCustomSettings,
...customProviderSettingsDefaults.anthropic,
...defaultProviderSettings.anthropic,
...voidInitModelOptions.anthropic,
enabled: false,
},
openAI: {
_enabled: undefined,
...defaultCustomSettings,
...customProviderSettingsDefaults.openAI,
...defaultProviderSettings.openAI,
...voidInitModelOptions.openAI,
enabled: false,
},
gemini: {
...defaultCustomSettings,
...customProviderSettingsDefaults.gemini,
...defaultProviderSettings.gemini,
...voidInitModelOptions.gemini,
enabled: false,
_enabled: undefined,
},
groq: {
...defaultCustomSettings,
...customProviderSettingsDefaults.groq,
...defaultProviderSettings.groq,
...voidInitModelOptions.groq,
enabled: false,
_enabled: undefined,
},
ollama: {
...defaultCustomSettings,
...customProviderSettingsDefaults.ollama,
...defaultProviderSettings.ollama,
...voidInitModelOptions.ollama,
enabled: false,
_enabled: undefined,
},
openRouter: {
...defaultCustomSettings,
...customProviderSettingsDefaults.openRouter,
...defaultProviderSettings.openRouter,
...voidInitModelOptions.openRouter,
enabled: false,
_enabled: undefined,
},
openAICompatible: {
...defaultCustomSettings,
...customProviderSettingsDefaults.openAICompatible,
...defaultProviderSettings.openAICompatible,
...voidInitModelOptions.openAICompatible,
enabled: false,
_enabled: undefined,
},
}
@@ -306,3 +372,43 @@ export type ModelSelectionOfFeature = {
export type FeatureName = keyof ModelSelectionOfFeature
export const featureNames = ['Ctrl+L', 'Ctrl+K', 'Autocomplete'] as const
// the models of these can be refreshed (in theory all can, but not all should)
export const refreshableProviderNames = localProviderNames
export type RefreshableProviderName = typeof refreshableProviderNames[number]
export type FeatureFlagSettings = {
autoRefreshModels: boolean;
}
export const defaultFeatureFlagSettings: FeatureFlagSettings = {
autoRefreshModels: true,
}
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}"`)
}
@@ -1,7 +1,7 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
/*--------------------------------------------------------------------------------------
* 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) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
/*--------------------------------------------------------------------------------------
* 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) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
/*--------------------------------------------------------------------------------------
* 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) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
/*--------------------------------------------------------------------------------------
* 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,14 +1,15 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
/*--------------------------------------------------------------------------------------
* 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 { _InternalOllamaListFnType, _InternalSendLLMMessageFnType, ModelResponse } from '../../common/llmMessageTypes.js';
import { _InternalModelListFnType, _InternalSendLLMMessageFnType, OllamaModelResponse } from '../../common/llmMessageTypes.js';
import { defaultProviderSettings } from '../../common/voidSettingsTypes.js';
export const ollamaList: _InternalOllamaListFnType = async ({ onSuccess: onSuccess_, onError: onError_, settingsOfProvider }) => {
export const ollamaList: _InternalModelListFnType<OllamaModelResponse> = async ({ onSuccess: onSuccess_, onError: onError_, settingsOfProvider }) => {
const onSuccess = ({ models }: { models: ModelResponse[] }) => {
const onSuccess = ({ models }: { models: OllamaModelResponse[] }) => {
onSuccess_({ models })
}
@@ -16,9 +17,11 @@ export const ollamaList: _InternalOllamaListFnType = async ({ onSuccess: onSucce
onError_({ error })
}
try {
const thisConfig = settingsOfProvider.ollama
// if endpoint is empty, normally ollama will send to 11434, but we want it to fail - the user should type it in
if (!thisConfig.endpoint) throw new Error(`Ollama Endpoint was empty (please enter ${defaultProviderSettings.ollama.endpoint} in Void if you want the default url).`)
const ollama = new Ollama({ host: thisConfig.endpoint })
ollama.list()
.then((response) => {
@@ -39,6 +42,8 @@ export const ollamaList: _InternalOllamaListFnType = async ({ onSuccess: onSucce
export const sendOllamaMsg: _InternalSendLLMMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => {
const thisConfig = settingsOfProvider.ollama
// if endpoint is empty, normally ollama will send to 11434, but we want it to fail - the user should type it in
if (!thisConfig.endpoint) throw new Error(`Ollama Endpoint was empty (please enter ${defaultProviderSettings.ollama.endpoint} if you want the default).`)
let fullText = ''
@@ -1,13 +1,49 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
/*--------------------------------------------------------------------------------------
* 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 { _InternalSendLLMMessageFnType } from '../../common/llmMessageTypes.js';
import { _InternalModelListFnType, _InternalSendLLMMessageFnType } from '../../common/llmMessageTypes.js';
import { Model } from 'openai/resources/models.js';
// import { parseMaxTokensStr } from './util.js';
export const openaiCompatibleList: _InternalModelListFnType<Model> = async ({ onSuccess: onSuccess_, onError: onError_, settingsOfProvider }) => {
const onSuccess = ({ models }: { models: Model[] }) => {
onSuccess_({ models })
}
const onError = ({ error }: { error: string }) => {
onError_({ error })
}
try {
const thisConfig = settingsOfProvider.openAICompatible
const openai = new OpenAI({ baseURL: thisConfig.endpoint, apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true })
openai.models.list()
.then(async (response) => {
const models: Model[] = []
models.push(...response.data)
while (response.hasNextPage()) {
models.push(...(await response.getNextPage()).data)
}
onSuccess({ models })
})
.catch((error) => {
onError({ error: error + '' })
})
}
catch (error) {
onError({ error: error + '' })
}
}
// OpenAI, OpenRouter, OpenAICompatible
export const sendOpenAIMsg: _InternalSendLLMMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }) => {
@@ -43,6 +79,7 @@ export const sendOpenAIMsg: _InternalSendLLMMessageFnType = ({ messages, onText,
throw new Error(`providerName was invalid: ${providerName}`)
}
openai.models.list()
openai.chat.completions
.create(options)
.then(async response => {
@@ -1,7 +1,7 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
/*--------------------------------------------------------------------------------------
* 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';
@@ -1,17 +1,18 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
/*--------------------------------------------------------------------------------------
* 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...
import { IServerChannel } from '../../../base/parts/ipc/common/ipc.js';
import { Emitter, Event } from '../../../base/common/event.js';
import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, MainLLMMessageParams, AbortRef, LLMMMessageParams, MainLLMMessageAbortParams, MainOllamaListParams, OllamaListParams, EventOllamaListOnSuccessParams, EventOllamaListOnErrorParams } from '../common/llmMessageTypes.js';
import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, MainLLMMessageParams, AbortRef, LLMMMessageParams, MainLLMMessageAbortParams, MainModelListParams, ModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, OllamaModelResponse, OpenaiCompatibleModelResponse, } from '../common/llmMessageTypes.js';
import { sendLLMMessage } from './llmMessage/sendLLMMessage.js'
import { IMetricsService } from '../common/metricsService.js';
import { ollamaList } from './llmMessage/ollama.js';
import { openaiCompatibleList } from './llmMessage/openai.js';
// NODE IMPLEMENTATION - calls actual sendLLMMessage() and returns listeners to it
@@ -25,8 +26,12 @@ export class LLMMessageChannel implements IServerChannel {
private readonly _abortRefOfRequestId_llm: Record<string, AbortRef> = {}
// ollamaList
private readonly _onSuccess_ollama = new Emitter<EventOllamaListOnSuccessParams>();
private readonly _onError_ollama = new Emitter<EventOllamaListOnErrorParams>();
private readonly _onSuccess_ollama = new Emitter<EventModelListOnSuccessParams<OllamaModelResponse>>();
private readonly _onError_ollama = new Emitter<EventModelListOnErrorParams<OllamaModelResponse>>();
// openaiCompatibleList
private readonly _onSuccess_openAICompatible = new Emitter<EventModelListOnSuccessParams<OpenaiCompatibleModelResponse>>();
private readonly _onError_openAICompatible = new Emitter<EventModelListOnErrorParams<OpenaiCompatibleModelResponse>>();
// stupidly, channels can't take in @IService
constructor(
@@ -50,6 +55,12 @@ export class LLMMessageChannel implements IServerChannel {
else if (event === 'onError_ollama') {
return this._onError_ollama.event;
}
else if (event === 'onSuccess_openAICompatible') {
return this._onSuccess_openAICompatible.event;
}
else if (event === 'onError_openAICompatible') {
return this._onError_openAICompatible.event;
}
else {
throw new Error(`Event not found: ${event}`);
}
@@ -67,6 +78,9 @@ export class LLMMessageChannel implements IServerChannel {
else if (command === 'ollamaList') {
this._callOllamaList(params)
}
else if (command === 'openAICompatibleList') {
this._callOpenAICompatibleList(params)
}
else {
throw new Error(`Void sendLLM: command "${command}" not recognized.`)
}
@@ -100,10 +114,10 @@ export class LLMMessageChannel implements IServerChannel {
delete this._abortRefOfRequestId_llm[requestId]
}
private _callOllamaList(params: MainOllamaListParams) {
private _callOllamaList(params: MainModelListParams<OllamaModelResponse>) {
const { requestId } = params;
const mainThreadParams: OllamaListParams = {
const mainThreadParams: ModelListParams<OllamaModelResponse> = {
...params,
onSuccess: ({ models }) => { this._onSuccess_ollama.fire({ requestId, models }); },
onError: ({ error }) => { this._onError_ollama.fire({ requestId, error }); },
@@ -111,5 +125,16 @@ export class LLMMessageChannel implements IServerChannel {
ollamaList(mainThreadParams)
}
private _callOpenAICompatibleList(params: MainModelListParams<OpenaiCompatibleModelResponse>) {
const { requestId } = params;
const mainThreadParams: ModelListParams<OpenaiCompatibleModelResponse> = {
...params,
onSuccess: ({ models }) => { this._onSuccess_openAICompatible.fire({ requestId, models }); },
onError: ({ error }) => { this._onError_openAICompatible.fire({ requestId, error }); },
}
openaiCompatibleList(mainThreadParams)
}
}
@@ -1,7 +1,7 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
/*--------------------------------------------------------------------------------------
* 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';
@@ -4,82 +4,113 @@
*--------------------------------------------------------------------------------------------*/
import { localize } from '../../../../nls.js';
import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js';
import { isMacintosh, isWeb, OS } from '../../../../base/common/platform.js';
import { Disposable, DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js';
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
import { IWorkspaceContextService, WorkbenchState } from '../../../../platform/workspace/common/workspace.js';
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
import { append, clearNode, $, h } from '../../../../base/browser/dom.js';
import { KeybindingLabel } from '../../../../base/browser/ui/keybindingLabel/keybindingLabel.js';
import { CommandsRegistry } from '../../../../platform/commands/common/commands.js';
import { ContextKeyExpr, ContextKeyExpression, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
import { defaultKeybindingLabelStyles } from '../../../../platform/theme/browser/defaultStyles.js';
import { editorForeground, registerColor, transparent } from '../../../../platform/theme/common/colorRegistry.js';
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
import { ColorScheme } from '../../../../platform/theme/common/theme.js';
import { isRecentFolder, IWorkspacesService } from '../../../../platform/workspaces/common/workspaces.js';
// import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
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 { 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.'));
interface WatermarkEntry {
readonly text: string;
readonly id: string;
readonly mac?: boolean;
readonly when?: ContextKeyExpression;
}
// interface WatermarkEntry {
// readonly text: string;
// readonly id: string;
// readonly mac?: boolean;
// readonly when?: ContextKeyExpression;
// }
const showCommands: WatermarkEntry = { text: localize('watermark.showCommands', "Show All Commands"), id: 'workbench.action.showCommands' };
const quickAccess: WatermarkEntry = { text: localize('watermark.quickAccess', "Go to File"), id: 'workbench.action.quickOpen' };
const openFileNonMacOnly: WatermarkEntry = { text: localize('watermark.openFile', "Open File"), id: 'workbench.action.files.openFile', mac: false };
const openFolderNonMacOnly: WatermarkEntry = { text: localize('watermark.openFolder', "Open Folder"), id: 'workbench.action.files.openFolder', mac: false };
const openFileOrFolderMacOnly: WatermarkEntry = { text: localize('watermark.openFileFolder', "Open File or Folder"), id: 'workbench.action.files.openFileFolder', mac: true };
const openRecent: WatermarkEntry = { text: localize('watermark.openRecent', "Open Recent"), id: 'workbench.action.openRecent' };
const newUntitledFileMacOnly: WatermarkEntry = { text: localize('watermark.newUntitledFile', "New Untitled Text File"), id: 'workbench.action.files.newUntitledFile', mac: true };
const findInFiles: WatermarkEntry = { text: localize('watermark.findInFiles', "Find in Files"), id: 'workbench.action.findInFiles' };
const toggleTerminal: WatermarkEntry = { text: localize({ key: 'watermark.toggleTerminal', comment: ['toggle is a verb here'] }, "Toggle Terminal"), id: 'workbench.action.terminal.toggleTerminal', when: ContextKeyExpr.equals('terminalProcessSupported', true) };
const startDebugging: WatermarkEntry = { text: localize('watermark.startDebugging', "Start Debugging"), id: 'workbench.action.debug.start', when: ContextKeyExpr.equals('terminalProcessSupported', true) };
const toggleFullscreen: WatermarkEntry = { text: localize({ key: 'watermark.toggleFullscreen', comment: ['toggle is a verb here'] }, "Toggle Full Screen"), id: 'workbench.action.toggleFullScreen' };
const showSettings: WatermarkEntry = { text: localize('watermark.showSettings', "Show Settings"), id: 'workbench.action.openSettings' };
// const showCommands: WatermarkEntry = { text: localize('watermark.showCommands', "Show All Commands"), id: 'workbench.action.showCommands' };
// const quickAccess: WatermarkEntry = { text: localize('watermark.quickAccess', "Go to File"), id: 'workbench.action.quickOpen' };
// const openFileNonMacOnly: WatermarkEntry = { text: localize('watermark.openFile', "Open File"), id: 'workbench.action.files.openFile', mac: false };
// const openFolderNonMacOnly: WatermarkEntry = { text: localize('watermark.openFolder', "Open Folder"), id: 'workbench.action.files.openFolder', mac: false };
// const openFileOrFolderMacOnly: WatermarkEntry = { text: localize('watermark.openFileFolder', "Open File or Folder"), id: 'workbench.action.files.openFileFolder', mac: true };
// const openRecent: WatermarkEntry = { text: localize('watermark.openRecent', "Open Recent"), id: 'workbench.action.openRecent' };
// const newUntitledFileMacOnly: WatermarkEntry = { text: localize('watermark.newUntitledFile', "New Untitled Text File"), id: 'workbench.action.files.newUntitledFile', mac: true };
// const findInFiles: WatermarkEntry = { text: localize('watermark.findInFiles', "Find in Files"), id: 'workbench.action.findInFiles' };
// const toggleTerminal: WatermarkEntry = { text: localize({ key: 'watermark.toggleTerminal', comment: ['toggle is a verb here'] }, "Toggle Terminal"), id: 'workbench.action.terminal.toggleTerminal', when: ContextKeyExpr.equals('terminalProcessSupported', true) };
// const startDebugging: WatermarkEntry = { text: localize('watermark.startDebugging', "Start Debugging"), id: 'workbench.action.debug.start', when: ContextKeyExpr.equals('terminalProcessSupported', true) };
// const toggleFullscreen: WatermarkEntry = { text: localize({ key: 'watermark.toggleFullscreen', comment: ['toggle is a verb here'] }, "Toggle Full Screen"), id: 'workbench.action.toggleFullScreen' };
// const showSettings: WatermarkEntry = { text: localize('watermark.showSettings', "Show Settings"), id: 'workbench.action.openSettings' };
const noFolderEntries = [
showCommands,
openFileNonMacOnly,
openFolderNonMacOnly,
openFileOrFolderMacOnly,
openRecent,
newUntitledFileMacOnly
];
// // shown when Void is emtpty
// const noFolderEntries = [
// // showCommands,
// openFileNonMacOnly,
// openFolderNonMacOnly,
// openFileOrFolderMacOnly,
// openRecent,
// // newUntitledFileMacOnly
// ];
const folderEntries = [
showCommands,
quickAccess,
findInFiles,
startDebugging,
toggleTerminal,
toggleFullscreen,
showSettings
];
// const folderEntries = [
// showCommands,
// // quickAccess,
// // findInFiles,
// // startDebugging,
// // toggleTerminal,
// // toggleFullscreen,
// // showSettings
// ];
export class EditorGroupWatermark extends Disposable {
private readonly shortcuts: HTMLElement;
private readonly transientDisposables = this._register(new DisposableStore());
private enabled: boolean = false;
// private enabled: boolean = false;
private workbenchState: WorkbenchState;
private keybindingLabels = new Set<KeybindingLabel>();
private currentDisposables = new Set<IDisposable>();
constructor(
container: HTMLElement,
@IKeybindingService private readonly keybindingService: IKeybindingService,
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
@IContextKeyService private readonly contextKeyService: IContextKeyService,
@IConfigurationService private readonly configurationService: IConfigurationService
// @IContextKeyService private readonly contextKeyService: IContextKeyService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IThemeService private readonly themeService: IThemeService,
@IWorkspacesService private readonly workspacesService: IWorkspacesService,
@ICommandService private readonly commandService: ICommandService,
@IHostService private readonly hostService: IHostService,
@ILabelService private readonly labelService: ILabelService,
) {
super();
const elements = h('.editor-group-watermark', [
h('.letterpress'),
h('.letterpress@icon'),
h('.shortcuts@shortcuts'),
]);
append(container, elements.root);
this.shortcuts = elements.shortcuts;
this.shortcuts = elements.shortcuts; // shortcuts div is modified on render()
// void icon style
const updateTheme = () => {
const theme = this.themeService.getColorTheme().type
const isDark = theme === ColorScheme.DARK || theme === ColorScheme.HIGH_CONTRAST_DARK
elements.icon.style.maxWidth = '220px'
elements.icon.style.opacity = '50%'
elements.icon.style.filter = isDark ? '' : 'invert(1)' //brightness(.5)
}
updateTheme()
this._register(
this.themeService.onDidColorThemeChange(updateTheme)
)
this.registerListeners();
@@ -103,56 +134,182 @@ export class EditorGroupWatermark extends Disposable {
this.render();
}));
const allEntriesWhenClauses = [...noFolderEntries, ...folderEntries].filter(entry => entry.when !== undefined).map(entry => entry.when!);
const allKeys = new Set<string>();
allEntriesWhenClauses.forEach(when => when.keys().forEach(key => allKeys.add(key)));
this._register(this.contextKeyService.onDidChangeContext(e => {
if (e.affectsSome(allKeys)) {
this.render();
}
}));
// const allEntriesWhenClauses = [...noFolderEntries, ...folderEntries].filter(entry => entry.when !== undefined).map(entry => entry.when!);
// const allKeys = new Set<string>();
// allEntriesWhenClauses.forEach(when => when.keys().forEach(key => allKeys.add(key)));
// this._register(this.contextKeyService.onDidChangeContext(e => {
// if (e.affectsSome(allKeys)) {
// this.render();
// }
// }));
}
private render(): void {
const enabled = this.configurationService.getValue<boolean>('workbench.tips.enabled');
// const enabled = this.configurationService.getValue<boolean>('workbench.tips.enabled');
if (enabled === this.enabled) {
return;
}
// if (enabled === this.enabled) {
// return;
// }
// this.enabled = enabled;
// if (!enabled) {
// return;
// }
// const hasFolder = this.workbenchState !== WorkbenchState.EMPTY;
// const selected = (hasFolder ? folderEntries : noFolderEntries)
// .filter(entry => !('when' in entry) || this.contextKeyService.contextMatchesRules(entry.when))
// .filter(entry => !('mac' in entry) || entry.mac === (isMacintosh && !isWeb))
// .filter(entry => !!CommandsRegistry.getCommand(entry.id))
// .filter(entry => !!this.keybindingService.lookupKeybinding(entry.id));
this.enabled = enabled;
this.clear();
if (!enabled) {
return;
}
const box = append(this.shortcuts, $('.watermark-box'));
const folder = this.workbenchState !== WorkbenchState.EMPTY;
const selected = (folder ? folderEntries : noFolderEntries)
.filter(entry => !('when' in entry) || this.contextKeyService.contextMatchesRules(entry.when))
.filter(entry => !('mac' in entry) || entry.mac === (isMacintosh && !isWeb))
.filter(entry => !!CommandsRegistry.getCommand(entry.id))
.filter(entry => !!this.keybindingService.lookupKeybinding(entry.id));
const boxBelow = append(this.shortcuts, $(''))
boxBelow.style.display = 'flex'
boxBelow.style.flex = 'row'
boxBelow.style.justifyContent = 'center'
const update = async () => {
const update = () => {
clearNode(box);
this.keybindingLabels.forEach(label => label.dispose());
this.keybindingLabels.clear();
clearNode(boxBelow);
for (const entry of selected) {
const keys = this.keybindingService.lookupKeybinding(entry.id);
if (!keys) {
continue;
this.currentDisposables.forEach(label => label.dispose());
this.currentDisposables.clear();
// Void - if the workbench is empty, show open
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 = () => {
this.commandService.executeCommand(isMacintosh && isNative ? OpenFileFolderAction.ID : OpenFolderAction.ID)
// if (this.contextKeyService.contextMatchesRules(ContextKeyExpr.and(WorkbenchStateContext.isEqualTo('workspace')))) {
// this.commandService.executeCommand(OpenFolderViaWorkspaceAction.ID);
// } else {
// this.commandService.executeCommand(isMacintosh ? 'workbench.action.files.openFileFolder' : 'workbench.action.files.openFolder');
// }
}
box.appendChild(button.root);
// Recents
const recentlyOpened = await this.workspacesService.getRecentlyOpened()
.catch(() => ({ files: [], workspaces: [] })).then(w => w.workspaces);
if (recentlyOpened.length !== 0) {
const span = $('div')
span.textContent = 'Recent'
span.style.fontWeight = '500'
box.append(span)
box.append(
...recentlyOpened.map(w => {
let fullPath: string;
let windowOpenable: IWindowOpenable;
if (isRecentFolder(w)) {
windowOpenable = { folderUri: w.folderUri };
fullPath = w.label || this.labelService.getWorkspaceLabel(w.folderUri, { verbose: Verbosity.LONG });
}
else {
return null
// fullPath = w.label || this.labelService.getWorkspaceLabel(w.workspace, { verbose: Verbosity.LONG });
// windowOpenable = { workspaceUri: w.workspace.configPath };
}
const { name, parentPath } = splitRecentLabel(fullPath);
const li = $('li');
const link = $('span');
link.classList.add('void-link')
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 => {
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
});
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);
return li
}).filter(v => !!v)
)
}
}
else {
// show them Void keybindings
const keys = this.keybindingService.lookupKeybinding(VOID_CTRL_L_ACTION_ID);
const dl = append(box, $('dl'));
const dt = append(dl, $('dt'));
dt.textContent = entry.text;
dt.textContent = 'Chat'
const dd = append(dl, $('dd'));
const label = new KeybindingLabel(dd, OS, { renderUnboundKeybindings: true, ...defaultKeybindingLabelStyles });
label.set(keys);
this.keybindingLabels.add(label);
if (keys)
label.set(keys);
this.currentDisposables.add(label);
const keys2 = this.keybindingService.lookupKeybinding(VOID_CTRL_K_ACTION_ID);
const dl2 = append(box, $('dl'));
const dt2 = append(dl2, $('dt'));
dt2.textContent = 'Quick Edit'
const dd2 = append(dl2, $('dd'));
const label2 = new KeybindingLabel(dd2, OS, { renderUnboundKeybindings: true, ...defaultKeybindingLabelStyles });
if (keys2)
label2.set(keys2);
this.currentDisposables.add(label2);
const keys3 = this.keybindingService.lookupKeybinding('workbench.action.openGlobalKeybindings');
const button3 = append(boxBelow, $('button'));
button3.textContent = 'Void Settings'
button3.style.display = 'block'
button3.style.marginLeft = 'auto'
button3.style.marginRight = 'auto'
button3.classList.add('void-settings-watermark-button')
const label3 = new KeybindingLabel(button3, OS, { renderUnboundKeybindings: true, ...defaultKeybindingLabelStyles });
if (keys3)
label3.set(keys3);
button3.onclick = () => {
this.commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID)
}
this.currentDisposables.add(label3);
}
};
update();
@@ -167,6 +324,6 @@ export class EditorGroupWatermark extends Disposable {
override dispose(): void {
super.dispose();
this.clear();
this.keybindingLabels.forEach(label => label.dispose());
this.currentDisposables.forEach(label => label.dispose());
}
}
@@ -9,13 +9,15 @@
height: 100%;
}
.monaco-workbench .part.editor > .content .editor-group-container.empty {
opacity: 0.5; /* dimmed to indicate inactive state */
.monaco-workbench .part.editor > .content .editor-group-container.empty {
opacity: 0.5;
/* dimmed to indicate inactive state */
}
.monaco-workbench .part.editor > .content .editor-group-container.empty.active,
.monaco-workbench .part.editor > .content .editor-group-container.empty.dragged-over {
opacity: 1; /* indicate active/dragged-over group through undimmed state */
opacity: 1;
/* indicate active/dragged-over group through undimmed state */
}
.monaco-workbench .part.editor > .content:not(.empty) .editor-group-container.empty.active:focus {
@@ -24,12 +26,13 @@
}
.monaco-workbench .part.editor > .content.empty .editor-group-container.empty.active:focus {
outline: none; /* never show outline for empty group if it is the last */
outline: none;
/* never show outline for empty group if it is the last */
}
/* Watermark & shortcuts */
.monaco-workbench .part.editor > .content .editor-group-container > .editor-group-watermark {
.monaco-workbench .part.editor > .content .editor-group-container > .editor-group-watermark {
display: flex;
height: 100%;
max-width: 290px;
@@ -49,26 +52,27 @@
height: calc(100% - 70px);
}
/* light */
.monaco-workbench .part.editor > .content .editor-group-container > .editor-group-watermark > .letterpress {
width: 100%;
max-height: 100%;
aspect-ratio: 1/1;
background-image: url('./letterpress-light.svg');
background-image: url('./void_cube_noshadow.png');
background-size: contain;
background-position-x: center;
background-repeat: no-repeat;
}
.monaco-workbench.vs-dark .part.editor > .content .editor-group-container .editor-group-watermark > .letterpress {
background-image: url('./letterpress-dark.svg');
background-image: url('./void_cube_noshadow.png');
}
.monaco-workbench.hc-light .part.editor > .content .editor-group-container .editor-group-watermark > .letterpress {
background-image: url('./letterpress-hcLight.svg');
background-image: url('./void_cube_noshadow.png');
}
.monaco-workbench.hc-black .part.editor > .content .editor-group-container .editor-group-watermark > .letterpress {
background-image: url('./letterpress-hcDark.svg');
background-image: url('./void_cube_noshadow.png');
}
.monaco-workbench .part.editor > .content:not(.empty) .editor-group-container > .editor-group-watermark > .shortcuts,
@@ -109,12 +113,13 @@
.monaco-workbench .part.editor > .content .editor-group-container > .title {
position: relative;
box-sizing: border-box;
box-sizing: border-box;
overflow: hidden;
}
.monaco-workbench .part.editor > .content .editor-group-container > .title:not(.tabs) {
display: flex; /* when tabs are not shown, use flex layout */
display: flex;
/* when tabs are not shown, use flex layout */
flex-wrap: nowrap;
}
@@ -144,7 +149,8 @@
.monaco-workbench .part.editor > .content .editor-group-container.empty.locked > .editor-group-container-toolbar,
.monaco-workbench .part.editor > .content:not(.empty) .editor-group-container.empty > .editor-group-container-toolbar,
.monaco-workbench .part.editor > .content.auxiliary .editor-group-container.empty > .editor-group-container-toolbar {
display: block; /* show toolbar when more than one editor group or always when auxiliary or locked */
display: block;
/* show toolbar when more than one editor group or always when auxiliary or locked */
}
.monaco-workbench .part.editor > .content .editor-group-container > .editor-group-container-toolbar .actions-container {
@@ -157,7 +163,7 @@
/* Editor */
.monaco-workbench .part.editor > .content .editor-group-container.empty > .editor-container {
.monaco-workbench .part.editor > .content .editor-group-container.empty > .editor-container {
display: none;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 850 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 795 KiB

@@ -1,7 +1,7 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
/*--------------------------------------------------------------------------------------
* 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,6 +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';
// The extension this was called from is here - https://github.com/voideditor/void/blob/autocomplete/extensions/void/src/extension/extension.ts
@@ -165,20 +166,6 @@ const postprocessResult = (result: string) => {
}
const extractCodeFromResult = (result: string) => {
// Match either:
// 1. ```language\n<code>```
// 2. ```<code>```
const match = result.match(/```(?:\w+\n)?([\s\S]*?)```|```([\s\S]*?)```/);
if (!match) {
return result;
}
// Return whichever group matched (non-empty)
return match[1] ?? match[2] ?? result;
}
// trims the end of the prefix to improve cache hit rate
const removeLeftTabsAndTrimEnd = (s: string): string => {
@@ -768,3 +755,5 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
registerSingleton(IAutocompleteService, AutocompleteService, InstantiationType.Eager);
@@ -0,0 +1,424 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { Disposable } from '../../../../../base/common/lifecycle.js';
import { URI } from '../../../../../base/common/uri.js';
import { generateUuid } from '../../../../../base/common/uuid.js';
import { ICodeEditor } from '../../../../../editor/browser/editorBrowser.js';
import { ICodeEditorService } from '../../../../../editor/browser/services/codeEditorService.js';
import { InstantiationType, registerSingleton } from '../../../../../platform/instantiation/common/extensions.js';
import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js';
// lets you add a "consistent" item to a Model (aka URI), instead of just to a single editor
type AddItemInputs = { uri: URI; fn: (editor: ICodeEditor) => (() => void); }
export interface IConsistentItemService {
readonly _serviceBrand: undefined;
getEditorsOnURI(uri: URI): ICodeEditor[];
addConsistentItemToURI(inputs: AddItemInputs): string;
removeConsistentItemFromURI(consistentItemId: string): void;
}
export const IConsistentItemService = createDecorator<IConsistentItemService>('ConsistentItemService');
export class ConsistentItemService extends Disposable {
readonly _serviceBrand: undefined
// the items that are attached to each URI, completely independent from current state of editors
private readonly consistentItemIdsOfURI: Record<string, Set<string> | undefined> = {}
private readonly infoOfConsistentItemId: Record<string, AddItemInputs> = {}
// current state of items on each editor, and the fns to call to remove them
private readonly itemIdsOfEditorId: Record<string, Set<string> | undefined> = {}
private readonly consistentItemIdOfItemId: Record<string, string> = {}
private readonly disposeFnOfItemId: Record<string, () => void> = {}
constructor(
@ICodeEditorService private readonly _editorService: ICodeEditorService,
) {
super()
const removeItemsFromEditor = (editor: ICodeEditor) => {
const editorId = editor.getId()
for (const itemId of this.itemIdsOfEditorId[editorId] ?? [])
this._removeItemFromEditor(editor, itemId)
}
// put items on the editor, based on the consistent items for that URI
const putItemsOnEditor = (editor: ICodeEditor, uri: URI | null) => {
if (!uri) return
for (const consistentItemId of this.consistentItemIdsOfURI[uri.fsPath] ?? [])
this._putItemOnEditor(editor, consistentItemId)
}
// when editor switches tabs (models)
const addTabSwitchListeners = (editor: ICodeEditor) => {
this._register(
editor.onDidChangeModel(e => {
removeItemsFromEditor(editor)
putItemsOnEditor(editor, e.newModelUrl)
})
)
}
// when editor is disposed
const addDisposeListener = (editor: ICodeEditor) => {
this._register(editor.onDidDispose(() => {
// anything on the editor has been disposed already
for (const itemId of this.itemIdsOfEditorId[editor.getId()] ?? [])
delete this.disposeFnOfItemId[itemId]
}))
}
const initializeEditor = (editor: ICodeEditor) => {
addTabSwitchListeners(editor)
addDisposeListener(editor)
putItemsOnEditor(editor, editor.getModel()?.uri ?? null)
}
// initialize current editors + any new editors
for (let editor of this._editorService.listCodeEditors()) initializeEditor(editor)
this._register(this._editorService.onCodeEditorAdd(editor => { initializeEditor(editor) }))
// when an editor is deleted, remove its items
this._register(this._editorService.onCodeEditorRemove(editor => {
removeItemsFromEditor(editor)
}))
}
_putItemOnEditor(editor: ICodeEditor, consistentItemId: string) {
const { fn } = this.infoOfConsistentItemId[consistentItemId]
// add item
const dispose = fn(editor)
const itemId = generateUuid()
const editorId = editor.getId()
if (!(editorId in this.itemIdsOfEditorId))
this.itemIdsOfEditorId[editorId] = new Set()
this.itemIdsOfEditorId[editorId]!.add(itemId)
this.consistentItemIdOfItemId[itemId] = consistentItemId
this.disposeFnOfItemId[itemId] = () => {
// console.log('calling remove for', itemId)
dispose?.()
}
}
_removeItemFromEditor(editor: ICodeEditor, itemId: string) {
const editorId = editor.getId()
this.itemIdsOfEditorId[editorId]?.delete(itemId)
this.disposeFnOfItemId[itemId]?.()
delete this.disposeFnOfItemId[itemId]
delete this.consistentItemIdOfItemId[itemId]
}
getEditorsOnURI(uri: URI) {
const editors = this._editorService.listCodeEditors().filter(editor => editor.getModel()?.uri.fsPath === uri.fsPath)
return editors
}
consistentItemIdPool = 0
addConsistentItemToURI({ uri, fn }: AddItemInputs) {
const consistentItemId = (this.consistentItemIdPool++) + ''
if (!(uri.fsPath in this.consistentItemIdsOfURI))
this.consistentItemIdsOfURI[uri.fsPath] = new Set()
this.consistentItemIdsOfURI[uri.fsPath]!.add(consistentItemId)
this.infoOfConsistentItemId[consistentItemId] = { fn, uri }
const editors = this.getEditorsOnURI(uri)
for (const editor of editors)
this._putItemOnEditor(editor, consistentItemId)
return consistentItemId
}
removeConsistentItemFromURI(consistentItemId: string) {
if (!(consistentItemId in this.infoOfConsistentItemId))
return
const { uri } = this.infoOfConsistentItemId[consistentItemId]
const editors = this.getEditorsOnURI(uri)
for (const editor of editors) {
for (const itemId of this.itemIdsOfEditorId[editor.getId()] ?? []) {
if (this.consistentItemIdOfItemId[itemId] === consistentItemId)
this._removeItemFromEditor(editor, itemId)
}
}
// clear
this.consistentItemIdsOfURI[uri.fsPath]?.delete(consistentItemId)
delete this.infoOfConsistentItemId[consistentItemId]
}
}
registerSingleton(IConsistentItemService, ConsistentItemService, InstantiationType.Eager);
// mostly generated by o1 (almost the same as above, but just for 1 editor)
export interface IConsistentEditorItemService {
readonly _serviceBrand: undefined;
addToEditor(editor: ICodeEditor, fn: () => () => void): string;
removeFromEditor(itemId: string): void;
}
export const IConsistentEditorItemService = createDecorator<IConsistentEditorItemService>('ConsistentEditorItemService');
export class ConsistentEditorItemService extends Disposable {
readonly _serviceBrand: undefined;
/**
* For each editorId, we track the set of itemIds that have been "added" to that editor.
* This does *not* necessarily mean they're currently mounted (the user may have switched models).
*/
private readonly itemIdsByEditorId: Record<string, Set<string>> = {};
/**
* For each itemId, we store relevant info (the fn to call on the editor, the editorId, the uri, and the current dispose function).
*/
private readonly itemInfoById: Record<
string,
{
editorId: string;
uriFsPath: string;
fn: (editor: ICodeEditor) => () => void;
disposeFn?: () => void;
}
> = {};
constructor(
@ICodeEditorService private readonly _editorService: ICodeEditorService,
) {
super();
//
// Wire up listeners to watch for new editors, removed editors, etc.
//
// Initialize any already-existing editors
for (const editor of this._editorService.listCodeEditors()) {
this._initializeEditor(editor);
}
// When an editor is added, track it
this._register(
this._editorService.onCodeEditorAdd((editor) => {
this._initializeEditor(editor);
})
);
// When an editor is removed, remove all items associated with that editor
this._register(
this._editorService.onCodeEditorRemove((editor) => {
this._removeAllItemsFromEditor(editor);
})
);
}
/**
* Sets up listeners on the provided editor so that:
* - If the editor changes models, we remove items and re-mount only if the new model matches.
* - If the editor is disposed, we do the needed cleanup.
*/
private _initializeEditor(editor: ICodeEditor) {
const editorId = editor.getId();
//
// Listen for model changes
//
this._register(
editor.onDidChangeModel((e) => {
this._removeAllItemsFromEditor(editor);
if (!e.newModelUrl) {
return;
}
// Re-mount any items that belong to this editor and match the new URI
const itemsForEditor = this.itemIdsByEditorId[editorId];
if (itemsForEditor) {
for (const itemId of itemsForEditor) {
const itemInfo = this.itemInfoById[itemId];
if (itemInfo && itemInfo.uriFsPath === e.newModelUrl.fsPath) {
this._mountItemOnEditor(editor, itemId);
}
}
}
})
);
//
// When the editor is disposed, remove all items from it
//
this._register(
editor.onDidDispose(() => {
this._removeAllItemsFromEditor(editor);
})
);
//
// If the editor already has a model (e.g. on initial load), try mounting items
//
const uri = editor.getModel()?.uri;
if (!uri) {
return;
}
const itemsForEditor = this.itemIdsByEditorId[editorId];
if (itemsForEditor) {
for (const itemId of itemsForEditor) {
const itemInfo = this.itemInfoById[itemId];
if (itemInfo && itemInfo.uriFsPath === uri.fsPath) {
this._mountItemOnEditor(editor, itemId);
}
}
}
}
/**
* Actually calls the item-creation function `fn(editor)` and saves the resulting disposeFn
* so we can later clean it up.
*/
private _mountItemOnEditor(editor: ICodeEditor, itemId: string) {
const info = this.itemInfoById[itemId];
if (!info) {
return;
}
const { fn } = info;
const disposeFn = fn(editor);
info.disposeFn = disposeFn;
}
/**
* Removes a single item from an editor (calling its `disposeFn` if present).
*/
private _removeItemFromEditor(editor: ICodeEditor, itemId: string) {
const info = this.itemInfoById[itemId];
if (info?.disposeFn) {
info.disposeFn();
info.disposeFn = undefined;
}
}
/**
* Removes *all* items from the given editor. Typically called when the editor changes model or is disposed.
*/
private _removeAllItemsFromEditor(editor: ICodeEditor) {
const editorId = editor.getId();
const itemsForEditor = this.itemIdsByEditorId[editorId];
if (!itemsForEditor) {
return;
}
for (const itemId of itemsForEditor) {
this._removeItemFromEditor(editor, itemId);
}
}
/**
* Public API: Adds an item to an *individual* editor (determined by editor ID),
* but only when that editor is showing the same model (uri.fsPath).
*/
addToEditor(editor: ICodeEditor, fn: () => () => void): string {
const uri = editor.getModel()?.uri
if (!uri) {
throw new Error('No URI on the provided editor or in AddItemInputs.');
}
const editorId = editor.getId();
// Create an ID for this item
const itemId = generateUuid();
// Record the info
this.itemInfoById[itemId] = {
editorId,
uriFsPath: uri.fsPath,
fn,
};
// Add to the editor's known items
if (!this.itemIdsByEditorId[editorId]) {
this.itemIdsByEditorId[editorId] = new Set();
}
this.itemIdsByEditorId[editorId].add(itemId);
// If the editor's current URI matches, mount it now
if (editor.getModel()?.uri.fsPath === uri.fsPath) {
this._mountItemOnEditor(editor, itemId);
}
return itemId;
}
/**
* Public API: Removes an item from the *specific* editor. We look up which editor
* had this item and remove it from that editor.
*/
removeFromEditor(itemId: string): void {
const info = this.itemInfoById[itemId];
if (!info) {
// Nothing to remove
return;
}
const { editorId } = info;
// Find the editor in question
const editor = this._editorService.listCodeEditors().find(
(ed) => ed.getId() === editorId
);
if (editor) {
// Dispose on that editor
this._removeItemFromEditor(editor, itemId);
}
// Clean up references
this.itemIdsByEditorId[editorId]?.delete(itemId);
delete this.itemInfoById[itemId];
}
}
registerSingleton(IConsistentEditorItemService, ConsistentEditorItemService, InstantiationType.Eager);
@@ -0,0 +1,18 @@
/*--------------------------------------------------------------------------------------
* 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) => {
// Match either:
// 1. ```language\n<code>```
// 2. ```<code>```
const match = result.match(/```(?:\w+\n)?([\s\S]*?)```|```([\s\S]*?)```/);
if (!match) {
return result;
}
// Return whichever group matched (non-empty)
return match[1] ?? match[2] ?? result;
}
@@ -1,7 +1,7 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
/*--------------------------------------------------------------------------------------
* 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,7 +1,7 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { isMacintosh } from '../../../../../base/common/platform.js';
@@ -1,51 +0,0 @@
import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js';
import { IModelService } from '../../../../../editor/common/services/model.js';
import { IClipboardService } from '../../../../../platform/clipboard/common/clipboardService.js';
import { IContextViewService, IContextMenuService } from '../../../../../platform/contextview/browser/contextView.js';
import { IFileService } from '../../../../../platform/files/common/files.js';
import { IHoverService } from '../../../../../platform/hover/browser/hover.js';
import { IThemeService } from '../../../../../platform/theme/common/themeService.js';
import { ILLMMessageService } from '../../../../../platform/void/common/llmMessageService.js';
import { IRefreshModelService } from '../../../../../platform/void/common/refreshModelService.js';
import { IVoidSettingsService } from '../../../../../platform/void/common/voidSettingsService.js';
import { IInlineDiffsService } from '../inlineDiffsService.js';
import { ISidebarStateService } from '../sidebarStateService.js';
import { IThreadHistoryService } from '../threadHistoryService.js';
export type ReactServicesType = {
sidebarStateService: ISidebarStateService;
settingsStateService: IVoidSettingsService;
threadsStateService: IThreadHistoryService;
fileService: IFileService;
modelService: IModelService;
inlineDiffService: IInlineDiffsService;
llmMessageService: ILLMMessageService;
clipboardService: IClipboardService;
refreshModelService: IRefreshModelService;
themeService: IThemeService,
hoverService: IHoverService,
contextViewService: IContextViewService;
contextMenuService: IContextMenuService;
}
export const getReactServices = (accessor: ServicesAccessor): ReactServicesType => {
return {
settingsStateService: accessor.get(IVoidSettingsService),
sidebarStateService: accessor.get(ISidebarStateService),
threadsStateService: accessor.get(IThreadHistoryService),
fileService: accessor.get(IFileService),
modelService: accessor.get(IModelService),
inlineDiffService: accessor.get(IInlineDiffsService),
llmMessageService: accessor.get(ILLMMessageService),
clipboardService: accessor.get(IClipboardService),
themeService: accessor.get(IThemeService),
hoverService: accessor.get(IHoverService),
refreshModelService: accessor.get(IRefreshModelService),
contextViewService: accessor.get(IContextViewService),
contextMenuService: accessor.get(IContextMenuService),
}
}
File diff suppressed because it is too large Load Diff
@@ -1,7 +1,7 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
/*--------------------------------------------------------------------------------------
* 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);
@@ -11,6 +11,10 @@
background-color: var(--vscode-void-sweepBG);
}
.void-highlightBG {
background-color: var(--vscode-void-highlightBG);
}
.void-greenBG {
background-color: var(--vscode-void-greenBG);
}
@@ -18,3 +22,52 @@
.void-redBG {
background-color: var(--vscode-void-redBG);
}
.void-watermark-button {
margin: 8px 0;
padding: 8px 20px;
background-color: #3b82f6;
color: white;
border: none;
border-radius: 4px;
outline: none !important;
box-shadow: none !important;
cursor: pointer;
transition: background-color 0.2s ease;
}
.void-watermark-button:hover {
background-color: #2563eb;
}
.void-watermark-button:active {
background-color: #2563eb;
}
.void-settings-watermark-button {
margin: 8px 0;
padding: 8px 20px;
background-color: var(--vscode-input-background);
color: var(--vscode-input-foreground);
border: none;
border-radius: 4px;
outline: none !important;
box-shadow: none !important;
cursor: pointer;
transition: all 0.2s ease;
}
.void-settings-watermark-button:hover {
filter: brightness(1.1);
}
.void-settings-watermark-button:active {
filter: brightness(1.1);
}
.void-link {
color: #3b82f6;
cursor: pointer;
}
@@ -0,0 +1,643 @@
/*--------------------------------------------------------------------------------------
* 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';
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\`.
Please edit the selected file following the user's instructions (or, if appropriate, answer their question instead).
Instructions:
1. Output the changes to make to the entire file.
1. Do not re-write the entire file.
3. Instead, you may use code elision to represent unchanged portions of code. For example, write "existing code..." in code comments.
4. You must give enough context to apply the change in the correct location.
5. Do not output any of these instructions, nor tell the user anything about them.
## EXAMPLE
FILES
selected file \`math.ts\`:
\`\`\`
const addNumbers = (a, b) => a + b
const subtractNumbers = (a, b) => a - b
const divideNumbers = (a, b) => a / b
\`\`\`
SELECTION
\`\`\`
const subtractNumbers = (a, b) => a - b
\`\`\`
INSTRUCTIONS
\`\`\`
add a function that multiplies numbers below this
\`\`\`
EXPECTED OUTPUT
We can add the following code to the file:
\`\`\`
// existing code...
const subtractNumbers = (a, b) => a - b;
const multiplyNumbers = (a, b) => a * b;
// existing code...
\`\`\`
## EXAMPLE
FILES
selected file \`fib.ts\`:
\`\`\`
const dfs = (root) => {
if (!root) return;
console.log(root.val);
dfs(root.left);
dfs(root.right);
}
const fib = (n) => {
if (n < 1) return 1
return fib(n - 1) + fib(n - 2)
}
\`\`\`
SELECTION
\`\`\`
return fib(n - 1) + fib(n - 2)
\`\`\`
INSTRUCTIONS
\`\`\`
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:
\`\`\`
// existing code...
const fib = (n, memo = {}) => {
if (n < 1) return 1;
if (memo[n]) return memo[n]; // Check if result is already computed
memo[n] = fib(n - 1, memo) + fib(n - 2, memo); // Store result in memo
return memo[n];
}
\`\`\`
Explanation:
Memoization Object: A memo object is used to store the results of Fibonacci calculations for each n.
Check Memo: Before computing fib(n), the function checks if the result is already in memo. If it is, it returns the stored result.
Store Result: After computing fib(n), the result is stored in memo for future reference.
## END EXAMPLES\
`
const stringifySelections = (selections: CodeSelection[]) => {
return selections.map(({ fileURI, content, selectionStr }) =>
`\
File: ${fileURI.fsPath}
\`\`\`
${content // this was the enite file which is foolish
}
\`\`\`${selectionStr === null ? '' : `
Selection: ${selectionStr}`}
`).join('\n')
}
export const chat_prompt = (instructions: string, selections: CodeSelection[] | null) => {
let str = '';
if (selections && selections.length > 0) {
str += stringifySelections(selections);
str += `Please edit the selected code following these instructions:\n`
}
str += `${instructions}`;
return str;
};
export const ctrlLStream_systemMessage = `
You are a coding assistant that applies a diff to a file. You are given the original file \`original_file\`, a diff \`diff\`, and a new file that you are applying the diff to \`new_file\`.
Please finish writing the new file \`new_file\`, according to the diff \`diff\`. You must completely re-write the whole file, using the diff.
Directions:
1. Continue exactly where the new file \`new_file\` left off.
2. Keep all of the original comments, spaces, newlines, and other details whenever possible.
3. Note that \`+\` lines represent additions, \`-\` lines represent removals, and space lines \` \` represent no change.
# Example 1:
ORIGINAL_FILE
\`Sidebar.tsx\`:
\`\`\`
import React from 'react';
import styles from './Sidebar.module.css';
interface SidebarProps {
items: { label: string; href: string }[];
onItemSelect?: (label: string) => void;
onExtraButtonClick?: () => void;
}
const Sidebar: React.FC<SidebarProps> = ({ items, onItemSelect, onExtraButtonClick }) => {
return (
<div className={styles.sidebar}>
<ul>
{items.map((item, index) => (
<li key={index}>
<button
className={styles.sidebarButton}
onClick={() => onItemSelect?.(item.label)}
>
{item.label}
</button>
</li>
))}
</ul>
<button className={styles.extraButton} onClick={onExtraButtonClick}>
Extra Action
</button>
</div>
);
};
export default Sidebar;
\`\`\`
DIFF
\`\`\`
@@ ... @@
-<div className={styles.sidebar}>
-<ul>
- {items.map((item, index) => (
- <li key={index}>
- <button
- className={styles.sidebarButton}
- onClick={() => onItemSelect?.(item.label)}
- >
- {item.label}
- </button>
- </li>
- ))}
-</ul>
-<button className={styles.extraButton} onClick={onExtraButtonClick}>
- Extra Action
-</button>
-</div>
+<div className={styles.sidebar}>
+<ul>
+ {items.map((item, index) => (
+ <li key={index}>
+ <div
+ className={styles.sidebarButton}
+ onClick={() => onItemSelect?.(item.label)}
+ >
+ {item.label}
+ </div>
+ </li>
+ ))}
+</ul>
+<div className={styles.extraButton} onClick={onExtraButtonClick}>
+ Extra Action
+</div>
+</div>
\`\`\`
NEW_FILE
\`\`\`
import React from 'react';
import styles from './Sidebar.module.css';
interface SidebarProps {
items: { label: string; href: string }[];
onItemSelect?: (label: string) => void;
onExtraButtonClick?: () => void;
}
const Sidebar: React.FC<SidebarProps> = ({ items, onItemSelect, onExtraButtonClick }) => {
return (
\`\`\`
COMPLETION
\`\`\`
<div className={styles.sidebar}>
<ul>
{items.map((item, index) => (
<li key={index}>
<div
className={styles.sidebarButton}
onClick={() => onItemSelect?.(item.label)}
>
{item.label}
</div>
</li>
))}
</ul>
<div className={styles.extraButton} onClick={onExtraButtonClick}>
Extra Action
</div>
</div>
);
};
export default Sidebar;\`\`\`
`
export const ctrlLStream_prompt = ({ originalCode, userMessage }: { originalCode: string, userMessage: string }) => {
return `\
ORIGINAL_CODE
\`\`\`
${originalCode}
\`\`\`
DIFF
\`\`\`
${userMessage}
\`\`\`
INSTRUCTIONS
Please finish writing the new file by applying the diff to the original file. Return ONLY the completion of the file, without any explanation.
`
}
export const ctrlKStream_systemMessage = `\
`
export const ctrlKStream_prefixAndSuffix = ({ fullFileStr, startLine, endLine }: { fullFileStr: string, startLine: number, endLine: number }) => {
const fullFileLines = fullFileStr.split('\n')
// we can optimize this later
const MAX_CHARS = 1024
/*
a
a
a <-- final i (prefix = a\na\n)
a
|b <-- startLine-1 (middle = b\nc\nd\n) <-- initial i (moves up)
c
d| <-- endLine-1 <-- initial j (moves down)
e
e <-- final j (suffix = e\ne\n)
e
e
*/
let prefix = ''
let i = startLine - 1 // 0-indexed exclusive
// 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
prefix = `${newLine}\n${prefix}`
i -= 1
}
else break
}
let suffix = ''
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
suffix = `${suffix}\n${newLine}`
j += 1
}
else break
}
return { prefix, suffix }
}
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:
\`\`\`
<${midTag}>${selection}</${midTag}>
\`\`\`
The user wants to apply the following instructions to the selection:
${userMessage}
Please rewrite the selection following the user's instructions.
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
Complete the following:
<${preTag}>${prefix}</${preTag}>
<${sufTag}>${suffix}</${sufTag}>
<${midTag}>`
}
};
// export const searchDiffChunkInstructions = `
// You are a coding assistant that applies a diff to a file. You are given a diff \`diff\`, a list of files \`files\` to apply the diff to, and a selection \`selection\` that you are currently considering in the file.
// Determine whether you should modify ANY PART of the selection \`selection\` following the \`diff\`. Return \`true\` if you should modify any part of the selection, and \`false\` if you should not modify any part of it.
// # Example 1:
// FILES
// selected file \`Sidebar.tsx\`:
// \`\`\`
// import React from 'react';
// import styles from './Sidebar.module.css';
// interface SidebarProps {
// items: { label: string; href: string }[];
// onItemSelect?: (label: string) => void;
// onExtraButtonClick?: () => void;
// }
// const Sidebar: React.FC<SidebarProps> = ({ items, onItemSelect, onExtraButtonClick }) => {
// return (
// <div className={styles.sidebar}>
// <ul>
// {items.map((item, index) => (
// <li key={index}>
// <button
// className={styles.sidebarButton}
// onClick={() => onItemSelect?.(item.label)}
// >
// {item.label}
// </button>
// </li>
// ))}
// </ul>
// <button className={styles.extraButton} onClick={onExtraButtonClick}>
// Extra Action
// </button>
// </div>
// );
// };
// export default Sidebar;
// \`\`\`
// DIFF
// \`\`\`
// @@ ... @@
// -<div className={styles.sidebar}>
// -<ul>
// - {items.map((item, index) => (
// - <li key={index}>
// - <button
// - className={styles.sidebarButton}
// - onClick={() => onItemSelect?.(item.label)}
// - >
// - {item.label}
// - </button>
// - </li>
// - ))}
// -</ul>
// -<button className={styles.extraButton} onClick={onExtraButtonClick}>
// - Extra Action
// -</button>
// -</div>
// +<div className={styles.sidebar}>
// +<ul>
// + {items.map((item, index) => (
// + <li key={index}>
// + <div
// + className={styles.sidebarButton}
// + onClick={() => onItemSelect?.(item.label)}
// + >
// + {item.label}
// + </div>
// + </li>
// + ))}
// +</ul>
// +<div className={styles.extraButton} onClick={onExtraButtonClick}>
// + Extra Action
// +</div>
// +</div>
// \`\`\`
// SELECTION
// \`\`\`
// import React from 'react';
// import styles from './Sidebar.module.css';
// interface SidebarProps {
// items: { label: string; href: string }[];
// onItemSelect?: (label: string) => void;
// onExtraButtonClick?: () => void;
// }
// const Sidebar: React.FC<SidebarProps> = ({ items, onItemSelect, onExtraButtonClick }) => {
// return (
// <div className={styles.sidebar}>
// <ul>
// {items.map((item, index) => (
// \`\`\`
// RESULT
// The output should be \`true\` because the diff begins on the line with \`<div className={styles.sidebar}>\` and this line is present in the selection.
// OUTPUT
// \`true\`
// `
// export const generateDiffInstructions = `
// 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\`.
// Please edit the selected file following the user's instructions (or, if appropriate, answer their question instead).
// All changes made to files must be outputted in unified diff format.
// Unified diff format instructions:
// 1. Each diff must begin with \`\`\`@@ ... @@\`\`\`.
// 2. Each line must start with a \`+\` or \`-\` or \` \` symbol.
// 3. Make diffs more than a few lines.
// 4. Make high-level diffs rather than many one-line diffs.
// Here's an example of unified diff format:
// \`\`\`
// @@ ... @@
// -def factorial(n):
// - if n == 0:
// - return 1
// - else:
// - return n * factorial(n-1)
// +def factorial(number):
// + if number == 0:
// + return 1
// + else:
// + return number * factorial(number-1)
// \`\`\`
// Please create high-level diffs where you group edits together if they are near each other, like in the above example. Another way to represent the above example is to make many small line edits. However, this is less preferred, because the edits are not high-level. The edits are close together and should be grouped:
// \`\`\`
// @@ ... @@ # This is less preferred because edits are close together and should be grouped:
// -def factorial(n):
// +def factorial(number):
// - if n == 0:
// + if number == 0:
// return 1
// else:
// - return n * factorial(n-1)
// + return number * factorial(number-1)
// \`\`\`
// # Example 1:
// FILES
// selected file \`test.ts\`:
// \`\`\`
// x = 1
// {{selection}}
// z = 3
// \`\`\`
// SELECTION
// \`\`\`const y = 2\`\`\`
// INSTRUCTIONS
// \`\`\`y = 3\`\`\`
// EXPECTED RESULT
// We should change the selection from \`\`\`y = 2\`\`\` to \`\`\`y = 3\`\`\`.
// \`\`\`
// @@ ... @@
// -x = 1
// -
// -y = 2
// +x = 1
// +
// +y = 3
// \`\`\`
// # Example 2:
// FILES
// selected file \`Sidebar.tsx\`:
// \`\`\`
// import React from 'react';
// import styles from './Sidebar.module.css';
// interface SidebarProps {
// items: { label: string; href: string }[];
// onItemSelect?: (label: string) => void;
// onExtraButtonClick?: () => void;
// }
// const Sidebar: React.FC<SidebarProps> = ({ items, onItemSelect, onExtraButtonClick }) => {
// return (
// <div className={styles.sidebar}>
// <ul>
// {items.map((item, index) => (
// <li key={index}>
// {{selection}}
// className={styles.sidebarButton}
// onClick={() => onItemSelect?.(item.label)}
// >
// {item.label}
// </button>
// </li>
// ))}
// </ul>
// <button className={styles.extraButton} onClick={onExtraButtonClick}>
// Extra Action
// </button>
// </div>
// );
// };
// export default Sidebar;
// \`\`\`
// SELECTION
// \`\`\` <button\`\`\`
// INSTRUCTIONS
// \`\`\`make all the buttons like this into divs\`\`\`
// EXPECTED OUTPUT
// We should change all the buttons like the one selected into a div component. Here is the change:
// \`\`\`
// @@ ... @@
// -<div className={styles.sidebar}>
// -<ul>
// - {items.map((item, index) => (
// - <li key={index}>
// - <button
// - className={styles.sidebarButton}
// - onClick={() => onItemSelect?.(item.label)}
// - >
// - {item.label}
// - </button>
// - </li>
// - ))}
// -</ul>
// -<button className={styles.extraButton} onClick={onExtraButtonClick}>
// - Extra Action
// -</button>
// -</div>
// +<div className={styles.sidebar}>
// +<ul>
// + {items.map((item, index) => (
// + <li key={index}>
// + <div
// + className={styles.sidebarButton}
// + onClick={() => onItemSelect?.(item.label)}
// + >
// + {item.label}
// + </div>
// + </li>
// + ))}
// +</ul>
// +<div className={styles.extraButton} onClick={onExtraButtonClick}>
// + Extra Action
// +</div>
// +</div>
// \`\`\`
// `;
@@ -1,32 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
import { CodeSelection } from '../threadHistoryService.js';
export const stringifySelections = (selections: CodeSelection[]) => {
return selections.map(({ fileURI, content, selectionStr }) =>
`\
File: ${fileURI.fsPath}
\`\`\`
${content // this was the enite file which is foolish
}
\`\`\`${selectionStr === null ? '' : `
Selection: ${selectionStr}`}
`).join('\n')
}
export const userInstructionsStr = (instructions: string, selections: CodeSelection[] | null) => {
let str = '';
if (selections && selections.length > 0) {
str += stringifySelections(selections);
str += `Please edit the selected code following these instructions:\n`
}
str += `${instructions}`;
return str;
};
@@ -1,523 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
// // used for ctrl+l
// const partialGenerationInstructions = ``
// // used for ctrl+k, autocomplete
// const fimInstructions = ``
// CTRL+K prompt:
// const promptContent = `Here is the user's original selection:
// \`\`\`
// <MID>${selection}</MID>
// \`\`\`
// The user wants to apply the following instructions to the selection:
// ${instructions}
// Please rewrite the selection following the user's instructions.
// 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
// Complete the following:
// \`\`\`
// <PRE>${prefix}</PRE>
// <SUF>${suffix}</SUF>
// <MID>`;
export const generateCtrlLInstructions = `\
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\`.
Please edit the selected file following the user's instructions (or, if appropriate, answer their question instead).
Instructions:
1. Output the changes to make to the entire file.
1. Do not re-write the entire file.
3. Instead, you may use code elision to represent unchanged portions of code. For example, write "existing code..." in code comments.
4. You must give enough context to apply the change in the correct location.
## EXAMPLE
FILES
selected file \`math.ts\`:
\`\`\`
const addNumbers = (a, b) => a + b
const subtractNumbers = (a, b) => a - b
const divideNumbers = (a, b) => a / b
\`\`\`
SELECTION
\`\`\`
const subtractNumbers = (a, b) => a - b
\`\`\`
INSTRUCTIONS
\`\`\`
add a function that multiplies numbers below this
\`\`\`
EXPECTED OUTPUT
We can add the following code to the file:
\`\`\`
// existing code...
const subtractNumbers = (a, b) => a - b;
const multiplyNumbers = (a, b) => a * b;
// existing code...
\`\`\`
## EXAMPLE
FILES
selected file \`fib.ts\`:
\`\`\`
const dfs = (root) => {
if (!root) return;
console.log(root.val);
dfs(root.left);
dfs(root.right);
}
const fib = (n) => {
if (n < 1) return 1
return fib(n - 1) + fib(n - 2)
}
\`\`\`
SELECTION
\`\`\`
return fib(n - 1) + fib(n - 2)
\`\`\`
INSTRUCTIONS
\`\`\`
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:
\`\`\`
// existing code...
const fib = (n, memo = {}) => {
if (n < 1) return 1;
if (memo[n]) return memo[n]; // Check if result is already computed
memo[n] = fib(n - 1, memo) + fib(n - 2, memo); // Store result in memo
return memo[n];
}
\`\`\`
Explanation:
Memoization Object: A memo object is used to store the results of Fibonacci calculations for each n.
Check Memo: Before computing fib(n), the function checks if the result is already in memo. If it is, it returns the stored result.
Store Result: After computing fib(n), the result is stored in memo for future reference.
## END EXAMPLES
`
export const generateDiffInstructions = `
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\`.
Please edit the selected file following the user's instructions (or, if appropriate, answer their question instead).
All changes made to files must be outputted in unified diff format.
Unified diff format instructions:
1. Each diff must begin with \`\`\`@@ ... @@\`\`\`.
2. Each line must start with a \`+\` or \`-\` or \` \` symbol.
3. Make diffs more than a few lines.
4. Make high-level diffs rather than many one-line diffs.
Here's an example of unified diff format:
\`\`\`
@@ ... @@
-def factorial(n):
- if n == 0:
- return 1
- else:
- return n * factorial(n-1)
+def factorial(number):
+ if number == 0:
+ return 1
+ else:
+ return number * factorial(number-1)
\`\`\`
Please create high-level diffs where you group edits together if they are near each other, like in the above example. Another way to represent the above example is to make many small line edits. However, this is less preferred, because the edits are not high-level. The edits are close together and should be grouped:
\`\`\`
@@ ... @@ # This is less preferred because edits are close together and should be grouped:
-def factorial(n):
+def factorial(number):
- if n == 0:
+ if number == 0:
return 1
else:
- return n * factorial(n-1)
+ return number * factorial(number-1)
\`\`\`
# Example 1:
FILES
selected file \`test.ts\`:
\`\`\`
x = 1
{{selection}}
z = 3
\`\`\`
SELECTION
\`\`\`const y = 2\`\`\`
INSTRUCTIONS
\`\`\`y = 3\`\`\`
EXPECTED RESULT
We should change the selection from \`\`\`y = 2\`\`\` to \`\`\`y = 3\`\`\`.
\`\`\`
@@ ... @@
-x = 1
-
-y = 2
+x = 1
+
+y = 3
\`\`\`
# Example 2:
FILES
selected file \`Sidebar.tsx\`:
\`\`\`
import React from 'react';
import styles from './Sidebar.module.css';
interface SidebarProps {
items: { label: string; href: string }[];
onItemSelect?: (label: string) => void;
onExtraButtonClick?: () => void;
}
const Sidebar: React.FC<SidebarProps> = ({ items, onItemSelect, onExtraButtonClick }) => {
return (
<div className={styles.sidebar}>
<ul>
{items.map((item, index) => (
<li key={index}>
{{selection}}
className={styles.sidebarButton}
onClick={() => onItemSelect?.(item.label)}
>
{item.label}
</button>
</li>
))}
</ul>
<button className={styles.extraButton} onClick={onExtraButtonClick}>
Extra Action
</button>
</div>
);
};
export default Sidebar;
\`\`\`
SELECTION
\`\`\` <button\`\`\`
INSTRUCTIONS
\`\`\`make all the buttons like this into divs\`\`\`
EXPECTED OUTPUT
We should change all the buttons like the one selected into a div component. Here is the change:
\`\`\`
@@ ... @@
-<div className={styles.sidebar}>
-<ul>
- {items.map((item, index) => (
- <li key={index}>
- <button
- className={styles.sidebarButton}
- onClick={() => onItemSelect?.(item.label)}
- >
- {item.label}
- </button>
- </li>
- ))}
-</ul>
-<button className={styles.extraButton} onClick={onExtraButtonClick}>
- Extra Action
-</button>
-</div>
+<div className={styles.sidebar}>
+<ul>
+ {items.map((item, index) => (
+ <li key={index}>
+ <div
+ className={styles.sidebarButton}
+ onClick={() => onItemSelect?.(item.label)}
+ >
+ {item.label}
+ </div>
+ </li>
+ ))}
+</ul>
+<div className={styles.extraButton} onClick={onExtraButtonClick}>
+ Extra Action
+</div>
+</div>
\`\`\`
`;
export const searchDiffChunkInstructions = `
You are a coding assistant that applies a diff to a file. You are given a diff \`diff\`, a list of files \`files\` to apply the diff to, and a selection \`selection\` that you are currently considering in the file.
Determine whether you should modify ANY PART of the selection \`selection\` following the \`diff\`. Return \`true\` if you should modify any part of the selection, and \`false\` if you should not modify any part of it.
# Example 1:
FILES
selected file \`Sidebar.tsx\`:
\`\`\`
import React from 'react';
import styles from './Sidebar.module.css';
interface SidebarProps {
items: { label: string; href: string }[];
onItemSelect?: (label: string) => void;
onExtraButtonClick?: () => void;
}
const Sidebar: React.FC<SidebarProps> = ({ items, onItemSelect, onExtraButtonClick }) => {
return (
<div className={styles.sidebar}>
<ul>
{items.map((item, index) => (
<li key={index}>
<button
className={styles.sidebarButton}
onClick={() => onItemSelect?.(item.label)}
>
{item.label}
</button>
</li>
))}
</ul>
<button className={styles.extraButton} onClick={onExtraButtonClick}>
Extra Action
</button>
</div>
);
};
export default Sidebar;
\`\`\`
DIFF
\`\`\`
@@ ... @@
-<div className={styles.sidebar}>
-<ul>
- {items.map((item, index) => (
- <li key={index}>
- <button
- className={styles.sidebarButton}
- onClick={() => onItemSelect?.(item.label)}
- >
- {item.label}
- </button>
- </li>
- ))}
-</ul>
-<button className={styles.extraButton} onClick={onExtraButtonClick}>
- Extra Action
-</button>
-</div>
+<div className={styles.sidebar}>
+<ul>
+ {items.map((item, index) => (
+ <li key={index}>
+ <div
+ className={styles.sidebarButton}
+ onClick={() => onItemSelect?.(item.label)}
+ >
+ {item.label}
+ </div>
+ </li>
+ ))}
+</ul>
+<div className={styles.extraButton} onClick={onExtraButtonClick}>
+ Extra Action
+</div>
+</div>
\`\`\`
SELECTION
\`\`\`
import React from 'react';
import styles from './Sidebar.module.css';
interface SidebarProps {
items: { label: string; href: string }[];
onItemSelect?: (label: string) => void;
onExtraButtonClick?: () => void;
}
const Sidebar: React.FC<SidebarProps> = ({ items, onItemSelect, onExtraButtonClick }) => {
return (
<div className={styles.sidebar}>
<ul>
{items.map((item, index) => (
\`\`\`
RESULT
The output should be \`true\` because the diff begins on the line with \`<div className={styles.sidebar}>\` and this line is present in the selection.
OUTPUT
\`true\`
`
export const writeFileWithDiffInstructions = `
You are a coding assistant that applies a diff to a file. You are given the original file \`original_file\`, a diff \`diff\`, and a new file that you are applying the diff to \`new_file\`.
Please finish writing the new file \`new_file\`, according to the diff \`diff\`. You must completely re-write the whole file, using the diff.
Directions:
1. Continue exactly where the new file \`new_file\` left off.
2. Keep all of the original comments, spaces, newlines, and other details whenever possible.
3. Note that \`+\` lines represent additions, \`-\` lines represent removals, and space lines \` \` represent no change.
# Example 1:
ORIGINAL_FILE
\`Sidebar.tsx\`:
\`\`\`
import React from 'react';
import styles from './Sidebar.module.css';
interface SidebarProps {
items: { label: string; href: string }[];
onItemSelect?: (label: string) => void;
onExtraButtonClick?: () => void;
}
const Sidebar: React.FC<SidebarProps> = ({ items, onItemSelect, onExtraButtonClick }) => {
return (
<div className={styles.sidebar}>
<ul>
{items.map((item, index) => (
<li key={index}>
<button
className={styles.sidebarButton}
onClick={() => onItemSelect?.(item.label)}
>
{item.label}
</button>
</li>
))}
</ul>
<button className={styles.extraButton} onClick={onExtraButtonClick}>
Extra Action
</button>
</div>
);
};
export default Sidebar;
\`\`\`
DIFF
\`\`\`
@@ ... @@
-<div className={styles.sidebar}>
-<ul>
- {items.map((item, index) => (
- <li key={index}>
- <button
- className={styles.sidebarButton}
- onClick={() => onItemSelect?.(item.label)}
- >
- {item.label}
- </button>
- </li>
- ))}
-</ul>
-<button className={styles.extraButton} onClick={onExtraButtonClick}>
- Extra Action
-</button>
-</div>
+<div className={styles.sidebar}>
+<ul>
+ {items.map((item, index) => (
+ <li key={index}>
+ <div
+ className={styles.sidebarButton}
+ onClick={() => onItemSelect?.(item.label)}
+ >
+ {item.label}
+ </div>
+ </li>
+ ))}
+</ul>
+<div className={styles.extraButton} onClick={onExtraButtonClick}>
+ Extra Action
+</div>
+</div>
\`\`\`
NEW_FILE
\`\`\`
import React from 'react';
import styles from './Sidebar.module.css';
interface SidebarProps {
items: { label: string; href: string }[];
onItemSelect?: (label: string) => void;
onExtraButtonClick?: () => void;
}
const Sidebar: React.FC<SidebarProps> = ({ items, onItemSelect, onExtraButtonClick }) => {
return (
\`\`\`
COMPLETION
\`\`\`
<div className={styles.sidebar}>
<ul>
{items.map((item, index) => (
<li key={index}>
<div
className={styles.sidebarButton}
onClick={() => onItemSelect?.(item.label)}
>
{item.label}
</div>
</li>
))}
</ul>
<div className={styles.extraButton} onClick={onExtraButtonClick}>
Extra Action
</div>
</div>
);
};
export default Sidebar;\`\`\`
`
@@ -0,0 +1,69 @@
/*--------------------------------------------------------------------------------------
* 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';
import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';
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';
export type QuickEditPropsType = {
diffareaid: number,
onGetInputBox: (i: InputBox) => void;
onChangeHeight: (height: number) => void;
onUserUpdateText: (text: string) => void;
initText: string | null;
}
export type QuickEdit = {
startLine: number, // 0-indexed
beforeCode: string,
afterCode?: string,
instructions?: string,
responseText?: string, // model can produce a text response too
}
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',
keybinding: {
primary: KeyMod.CtrlCmd | KeyCode.KeyK,
weight: KeybindingWeight.BuiltinExtension,
}
});
}
async run(accessor: ServicesAccessor): Promise<void> {
const editorService = accessor.get(ICodeEditorService)
const metricsService = accessor.get(IMetricsService)
metricsService.capture('User Action', { type: 'Open Ctrl+K' })
const editor = editorService.getActiveCodeEditor()
if (!editor) return;
const model = editor.getModel()
if (!model) return;
const selection = editor.getSelection()
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 })
}
});
@@ -0,0 +1,77 @@
/*--------------------------------------------------------------------------------------
* 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';
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { QuickEdit } from './quickEditActions.js';
// service that manages state
export type VoidQuickEditState = {
quickEditsOfDocument: { [uri: string]: QuickEdit }
}
export interface IQuickEditStateService {
readonly _serviceBrand: undefined;
readonly state: VoidQuickEditState; // readonly to the user
setState(newState: Partial<VoidQuickEditState>): void;
onDidChangeState: Event<void>;
onDidFocusChat: Event<void>;
onDidBlurChat: Event<void>;
fireFocusChat(): void;
fireBlurChat(): void;
}
export const IQuickEditStateService = createDecorator<IQuickEditStateService>('voidQuickEditStateService');
class VoidQuickEditStateService extends Disposable implements IQuickEditStateService {
_serviceBrand: undefined;
static readonly ID = 'voidQuickEditStateService';
private readonly _onDidChangeState = new Emitter<void>();
readonly onDidChangeState: Event<void> = this._onDidChangeState.event;
private readonly _onFocusChat = new Emitter<void>();
readonly onDidFocusChat: Event<void> = this._onFocusChat.event;
private readonly _onBlurChat = new Emitter<void>();
readonly onDidBlurChat: Event<void> = this._onBlurChat.event;
// state
state: VoidQuickEditState
constructor(
) {
super()
// initial state
this.state = { quickEditsOfDocument: {} }
}
setState(newState: Partial<VoidQuickEditState>) {
this.state = { ...this.state, ...newState }
this._onDidChangeState.fire()
}
fireFocusChat() {
this._onFocusChat.fire()
}
fireBlurChat() {
this._onBlurChat.fire()
}
}
registerSingleton(IQuickEditStateService, VoidQuickEditStateService, InstantiationType.Eager);
@@ -1,18 +1,137 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { execSync } from 'child_process';
import { spawn, execSync } from 'child_process';
// Added lines below
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
// clear temp dirs
execSync('npx rimraf out/ && npx rimraf src2/')
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// build and scope tailwind: https://www.npmjs.com/package/scope-tailwind
execSync('npx scope-tailwind ./src -o src2/ -s void-scope -c styles.css -p "void-" ')
function doesPathExist(filePath) {
try {
const stats = fs.statSync(filePath);
// tsup to build src2/ into out/
execSync('npx tsup')
return stats.isFile();
} catch (err) {
if (err.code === 'ENOENT') {
return false;
}
throw err;
}
}
/*
console.log('✅ Done building! Kill your build script(s) (Ctrl+D in them), then press Cmd+Shift+B again.')
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 {
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(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);
}
const args = process.argv.slice(2);
const isWatch = args.includes('--watch') || args.includes('-w');
if (isWatch) {
// Watch mode
const scopeTailwindWatcher = spawn('npx', [
'nodemon',
'--watch', 'src',
'--ext', 'ts,tsx,css',
'--exec',
'npx scope-tailwind ./src -o src2/ -s void-scope -c styles.css -p "void-"'
]);
const tsupWatcher = spawn('npx', [
'tsup',
'--watch'
]);
scopeTailwindWatcher.stdout.on('data', (data) => {
console.log(`[scope-tailwind] ${data}`);
// If the output mentions "styles.css", trigger the save:
if (data.toString().includes('styles.css')) {
saveStylesFile();
}
});
scopeTailwindWatcher.stderr.on('data', (data) => {
console.error(`[scope-tailwind] ${data}`);
});
// Handle tsup watcher output
tsupWatcher.stdout.on('data', (data) => {
console.log(`[tsup] ${data}`);
});
tsupWatcher.stderr.on('data', (data) => {
console.error(`[tsup] ${data}`);
});
// Handle process termination
process.on('SIGINT', () => {
scopeTailwindWatcher.kill();
tsupWatcher.kill();
process.exit();
});
console.log('🔄 Watchers started! Press Ctrl+C to stop both watchers.');
} else {
// Build mode
console.log('📦 Building...');
// Run scope-tailwind once
execSync('npx scope-tailwind ./src -o src2/ -s void-scope -c styles.css -p "void-"', { stdio: 'inherit' });
// Run tsup once
execSync('npx tsup', { stdio: 'inherit' });
console.log('✅ Build complete!');
}
@@ -0,0 +1,23 @@
/*--------------------------------------------------------------------------------------
* 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 { useIsDark, useSidebarState } from '../util/services.js'
import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js'
import { CtrlKChat } from './CtrlKChat.js'
import { QuickEditPropsType } from '../../../quickEditActions.js'
export const CtrlK = (props: QuickEditPropsType) => {
const isDark = useIsDark()
return <div className={`@@void-scope ${isDark ? 'dark' : ''}`}>
<ErrorBoundary>
<CtrlKChat {...props} />
</ErrorBoundary>
</div>
}
@@ -0,0 +1,177 @@
/*--------------------------------------------------------------------------------------
* 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, 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>
}
@@ -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 { CtrlK } from './CtrlK.js'
export const mountCtrlK = mountFnGenerator(CtrlK)
@@ -1,7 +1,7 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
/*--------------------------------------------------------------------------------------
* 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,44 +1,85 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import React, { ReactNode } from "react"
import SyntaxHighlighter from "react-syntax-highlighter";
import { atomOneDarkReasonable } from "react-syntax-highlighter/dist/esm/styles/hljs";
import { ReactNode } from "react"
import { VoidCodeEditor } 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',
// 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',
// 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 customStyle = {
...atomOneDarkReasonable,
'code[class*="language-"]': {
...atomOneDarkReasonable['code[class*="language-"]'],
background: "none",
},
}
const isSingleLine = !text.includes('\n')
return (<>
<div className={`relative group w-full bg-vscode-sidebar-bg overflow-hidden isolate`}>
<div className={`relative group w-full bg-vscode-editor-bg overflow-hidden isolate`}>
{buttonsOnHover === null ? null : (
<div className="absolute top-0 right-0 opacity-0 group-hover:opacity-100 duration-200">
<div className="flex space-x-2 p-2">{buttonsOnHover}</div>
<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>
)}
<div
className={`overflow-x-auto rounded-sm text-vscode-editor-fg bg-vscode-editor-bg`}
>
<SyntaxHighlighter
language={language ?? 'plaintext'} // TODO must auto detect language
style={customStyle}
className={"rounded-sm"}
>
{text}
</SyntaxHighlighter>
</div>
<VoidCodeEditor
initValue={text}
language={language}
/>
</div>
</>
)
@@ -1,12 +1,12 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
/*--------------------------------------------------------------------------------------
* 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 { useService } from '../util/services.js'
import { useAccessor } from '../util/services.js'
enum CopyButtonState {
@@ -17,14 +17,14 @@ enum CopyButtonState {
const COPY_FEEDBACK_TIMEOUT = 1000 // amount of time to say 'Copied!'
const CodeButtonsOnHover = ({ diffRepr: text }: { diffRepr: string }) => {
const CodeButtonsOnHover = ({ text }: { text: string }) => {
const accessor = useAccessor()
const [copyButtonState, setCopyButtonState] = useState(CopyButtonState.Copy)
const inlineDiffService = useService('inlineDiffService')
const clipboardService = useService('clipboardService')
const inlineDiffService = accessor.get('IInlineDiffsService')
const clipboardService = accessor.get('IClipboardService')
useEffect(() => {
if (copyButtonState !== CopyButtonState.Copy) {
setTimeout(() => {
setCopyButtonState(CopyButtonState.Copy)
@@ -38,19 +38,26 @@ const CodeButtonsOnHover = ({ diffRepr: text }: { diffRepr: string }) => {
.catch(() => { setCopyButtonState(CopyButtonState.Error) })
}, [text, clipboardService])
const onApply = useCallback(() => {
inlineDiffService.startApplying({
featureName: 'Ctrl+L',
userMessage: text,
})
}, [inlineDiffService])
const isSingleLine = !text.includes('\n')
return <>
<button
className="btn btn-secondary btn-sm border 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`}
onClick={onCopy}
>
{copyButtonState}
</button>
<button
className="btn btn-secondary btn-sm border border-vscode-input-border rounded"
onClick={async () => {
inlineDiffService.startStreaming({ featureName: 'Ctrl+L' }, text)
}}
// 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`}
onClick={onApply}
>
Apply
</button>
@@ -70,8 +77,8 @@ const RenderToken = ({ token, nested = false }: { token: Token | string, nested?
if (t.type === "code") {
return <BlockCode
text={t.text}
language={t.lang}
buttonsOnHover={<CodeButtonsOnHover diffRepr={t.text} />}
// language={t.lang} // instead use vscode to detect language
buttonsOnHover={<CodeButtonsOnHover text={t.text} />}
/>
}
@@ -169,7 +176,7 @@ const RenderToken = ({ token, nested = false }: { token: Token | string, nested?
if (t.type === "link") {
return (
<a 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>
)
@@ -190,7 +197,7 @@ const RenderToken = ({ token, nested = false }: { token: Token | string, nested?
// inline code
if (t.type === "codespan") {
return (
<code className="text-vscode-editor-fg bg-vscode-editor-bg px-1 rounded-sm font-mono">
<code className="text-vscode-text-preformat-fg bg-vscode-text-preformat-bg px-1 rounded-sm font-mono">
{t.text}
</code>
)
@@ -1,7 +1,7 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
/*--------------------------------------------------------------------------------------
* 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,7 +1,7 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
/*--------------------------------------------------------------------------------------
* 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 { AlertCircle, ChevronDown, ChevronUp, X } from 'lucide-react';
@@ -1,7 +1,7 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
/*--------------------------------------------------------------------------------------
* 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'
@@ -22,8 +22,9 @@ export const Sidebar = ({ className }: { className: string }) => {
const { isHistoryOpen, currentTab: tab } = sidebarState
const isDark = useIsDark()
return <div className={`@@void-scope ${isDark ? 'dark' : ''}`} style={{ width: '100%', height: '100%' }}>
<div className={`flex flex-col px-2 py-2 w-full h-full`}>
// ${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`}>
{/* <span onClick={() => {
const tabs = ['chat', 'settings', 'threadSelector']
@@ -31,7 +32,7 @@ export const Sidebar = ({ className }: { className: string }) => {
sidebarStateService.setState({ currentTab: tabs[(index + 1) % tabs.length] as any })
}}>clickme {tab}</span> */}
<div className={`mb-2 w-full ${isHistoryOpen ? '' : 'hidden'}`}>
<div className={`w-full h-auto mb-2 ${isHistoryOpen ? '' : 'hidden'} ring-2 ring-widget-shadow z-10`}>
<ErrorBoundary>
<SidebarThreadSelector />
</ErrorBoundary>
@@ -1,19 +1,16 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
/*--------------------------------------------------------------------------------------
* 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, Fragment, useCallback, useEffect, useRef, useState } from 'react';
import React, { ButtonHTMLAttributes, FormEvent, FormHTMLAttributes, Fragment, useCallback, useEffect, useRef, useState } from 'react';
import { useSettingsState, useService, useSidebarState, useThreadsState } from '../util/services.js';
import { generateCtrlLInstructions, generateDiffInstructions } from '../../../prompt/systemPrompts.js';
import { userInstructionsStr } from '../../../prompt/stringifySelections.js';
import { ChatMessage, CodeSelection, CodeStagingSelection } from '../../../threadHistoryService.js';
import { useAccessor, useThreadsState } from '../util/services.js';
import { ChatMessage, CodeSelection, CodeStagingSelection, IThreadHistoryService } from '../../../threadHistoryService.js';
import { BlockCode } from '../markdown/BlockCode.js';
import { BlockCode, getLanguageFromFileName } from '../markdown/BlockCode.js';
import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js';
import { IModelService } from '../../../../../../../editor/common/services/model.js';
import { URI } from '../../../../../../../base/common/uri.js';
import { EndOfLinePreference } from '../../../../../../../editor/common/model.js';
import { IDisposable } from '../../../../../../../base/common/lifecycle.js';
@@ -23,9 +20,13 @@ 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 { 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';
const IconX = ({ size, className = '' }: { size: number, className?: string }) => {
const IconX = ({ size, className = '', ...props }: { size: number, className?: string } & React.SVGProps<SVGSVGElement>) => {
return (
<svg
xmlns='http://www.w3.org/2000/svg'
@@ -33,8 +34,9 @@ const IconX = ({ size, className = '' }: { size: number, className?: string }) =
height={size}
viewBox='0 0 24 24'
fill='none'
stroke='black'
stroke='currentColor'
className={className}
{...props}
>
<path
strokeLinecap='round'
@@ -86,6 +88,116 @@ const IconSquare = ({ size, className = '' }: { size: number, className?: string
};
export const IconWarning = ({ size, className = '' }: { size: number, className?: string }) => {
return (
<svg
className={className}
stroke="currentColor"
fill="currentColor"
strokeWidth="0"
viewBox="0 0 16 16"
width={size}
height={size}
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M7.56 1h.88l6.54 12.26-.44.74H1.44L1 13.26 7.56 1zM8 2.28L2.28 13H13.7L8 2.28zM8.625 12v-1h-1.25v1h1.25zm-1.25-2V6h1.25v4h-1.25z"
/>
</svg>
);
};
export const IconLoading = ({ className = '' }: { className?: string }) => {
const [loadingText, setLoadingText] = useState('.');
useEffect(() => {
let intervalId;
// Function to handle the animation
const toggleLoadingText = () => {
if (loadingText === '...') {
setLoadingText('.');
} else {
setLoadingText(loadingText + '.');
}
};
// Start the animation loop
intervalId = setInterval(toggleLoadingText, 300);
// Cleanup function to clear the interval when component unmounts
return () => clearInterval(intervalId);
}, [loadingText, setLoadingText]);
return <div className={`${className}`}>{loadingText}</div>;
}
const useResizeObserver = () => {
const ref = useRef(null);
const [dimensions, setDimensions] = useState({ height: 0, width: 0 });
useEffect(() => {
if (ref.current) {
const resizeObserver = new ResizeObserver((entries) => {
if (entries.length > 0) {
const entry = entries[0];
setDimensions({
height: entry.contentRect.height,
width: entry.contentRect.width
});
}
});
resizeObserver.observe(ref.current);
return () => {
if (ref.current)
resizeObserver.unobserve(ref.current);
};
}
}, []);
return [ref, dimensions] as const;
};
type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement>
const DEFAULT_BUTTON_SIZE = 20;
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'}
${className}
`}
{...props}
>
<IconArrowUp size={DEFAULT_BUTTON_SIZE} className="stroke-[2]" />
</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}
`}
type='button'
{...props}
>
<IconSquare size={DEFAULT_BUTTON_SIZE} className="stroke-[2] p-[6px]" />
</button>
}
const ScrollToBottomContainer = ({ children, className, style }: { children: React.ReactNode, className?: string, style?: React.CSSProperties }) => {
const [isAtBottom, setIsAtBottom] = useState(true); // Start at bottom
const divRef = useRef<HTMLDivElement>(null);
@@ -160,24 +272,27 @@ export const SelectedFiles = (
return (
!!selections && selections.length !== 0 && (
<div
className='flex flex-wrap gap-4 p-2 text-left'
className='flex flex-wrap gap-2 text-left'
>
{selections.map((selection, i) => {
const showSelectionText = selection.selectionStr && selectionIsOpened[i]
const isThisSelectionOpened = !!(selection.selectionStr && selectionIsOpened[i])
return (
<div key={i} // container for `selectionSummary` and `selectionText`
className={`${showSelectionText ? 'w-full' : ''}`}
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={`grid grid-rows-2 gap-1 relative
className={`flex items-center gap-1 relative
rounded-md p-1
w-fit h-fit
select-none
bg-vscode-badge-bg border border-vscode-button-border rounded-md
w-fit h-fit min-w-[81px] p-1
`}
bg-vscode-editor-bg hover:brightness-95
border border-vscode-commandcenter-border rounded-xs
text-xs text-vscode-editor-fg text-nowrap
`}
onClick={() => {
setSelectionIsOpened(s => {
const newS = [...s]
@@ -186,18 +301,37 @@ export const SelectedFiles = (
});
}}
>
<span className='truncate'>
<span className=''>
{/* file name */}
{getBasename(selection.fileURI.fsPath)}
{/* selection range */}
{selection.selectionStr !== null ? ` (${selection.range.startLineNumber}-${selection.range.endLineNumber})` : ''}
</span>
{/* type of selection */}
<span className='truncate text-opacity-75'>{selection.selectionStr !== null ? 'Selection' : 'File'}</span>
{/* X button */}
{type === 'staging' && // hoveredIdx === i
{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>
}
{/* 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();
@@ -208,12 +342,13 @@ export const SelectedFiles = (
>
<IconX size={16} className="p-[2px] stroke-[3]" />
</span>
}
} */}
</div>
{/* selection text */}
{showSelectionText &&
<div className='w-full'>
<BlockCode text={selection.selectionStr!} />
{isThisSelectionOpened &&
<div className='w-full p-1 rounded-sm border-vscode-editor-border'>
<BlockCode text={selection.selectionStr!} language={getLanguageFromFileName(selection.fileURI.path)} />
</div>
}
</div>
@@ -225,8 +360,9 @@ export const SelectedFiles = (
}
const ChatBubble = ({ chatMessage }: {
chatMessage: ChatMessage
const ChatBubble = ({ chatMessage, isLoading }: {
chatMessage: ChatMessage,
isLoading?: boolean,
}) => {
const role = chatMessage.role
@@ -246,10 +382,16 @@ const ChatBubble = ({ chatMessage }: {
chatbubbleContents = <ChatMarkdownRender string={chatMessage.displayContent} /> // sectionsHTML
}
return <div className={`${role === 'user' ? 'text-right' : 'text-left'}`}>
<div className={`inline-block p-2 rounded-lg space-y-2 ${role === 'user' ? 'bg-vscode-input-bg text-vscode-input-fg' : ''} max-w-full overflow-auto`}>
{chatbubbleContents}
</div>
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' : ''}
`}
>
{chatbubbleContents}
{isLoading && <IconLoading className='opacity-50 text-sm' />}
</div>
}
@@ -259,11 +401,12 @@ export const SidebarChat = () => {
const inputBoxRef: React.MutableRefObject<InputBox | null> = useRef(null);
const modelService = useService('modelService')
const accessor = useAccessor()
const modelService = accessor.get('IModelService')
// ----- HIGHER STATE -----
// sidebar state
const sidebarStateService = useService('sidebarStateService')
const sidebarStateService = accessor.get('ISidebarStateService')
useEffect(() => {
const disposables: IDisposable[] = []
disposables.push(
@@ -275,7 +418,9 @@ export const SidebarChat = () => {
// threads state
const threadsState = useThreadsState()
const threadsStateService = useService('threadsStateService')
const threadsStateService = accessor.get('IThreadHistoryService')
const llmMessageService = accessor.get('ILLMMessageService')
// ----- SIDEBAR CHAT state (local) -----
@@ -286,13 +431,16 @@ export const SidebarChat = () => {
const [latestError, setLatestError] = useState<Parameters<OnError>[0] | null>(null)
const llmMessageService = useService('llmMessageService')
// state of current message
const [instructions, setInstructions] = useState('') // the user's instructions
const isDisabled = !instructions.trim()
const [formHeight, setFormHeight] = useState(0) // TODO should use resize observer instead
const [sidebarHeight, setSidebarHeight] = useState(0)
const [sidebarRef, sidebarDimensions] = useResizeObserver()
const [formRef, formDimensions] = 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])
@@ -325,11 +473,11 @@ export const SidebarChat = () => {
// add system message to chat history
const systemPromptElt: ChatMessage = { role: 'system', content: generateCtrlLInstructions }
const systemPromptElt: ChatMessage = { role: 'system', content: chat_systemMessage }
threadsStateService.addMessageToCurrentThread(systemPromptElt)
// add user's message to chat history
const userHistoryElt: ChatMessage = { role: 'user', content: userInstructionsStr(instructions, selections), displayContent: instructions, selections: selections }
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
@@ -377,6 +525,8 @@ export const SidebarChat = () => {
threadsStateService.setStaging([]) // clear staging
inputBoxRef.current?.focus() // focus input after submit
}
const onAbort = () => {
@@ -403,18 +553,23 @@ export const SidebarChat = () => {
// const [_test_messages, _set_test_messages] = useState<string[]>([])
return <div
ref={(ref) => { if (ref) { setSidebarHeight(ref.clientHeight); } }}
ref={sidebarRef}
className={`w-full h-full`}
>
<ScrollToBottomContainer
className={`overflow-x-hidden overflow-y-auto`}
style={{ maxHeight: sidebarHeight - formHeight - 30 }}
className={`
w-full h-auto
flex flex-col gap-0
overflow-x-hidden
overflow-y-auto
`}
style={{ maxHeight: sidebarDimensions.height - formDimensions.height - 30 }}
>
{/* previous messages */}
{previousMessages.map((message, i) => <ChatBubble key={i} chatMessage={message} />)}
{/* message stream */}
<ChatBubble chatMessage={{ role: 'assistant', content: messageStream, displayContent: messageStream || null }} />
<ChatBubble chatMessage={{ role: 'assistant', content: messageStream, displayContent: messageStream || null }} isLoading={isLoading} />
{/* {_test_messages.map((_, i) => <div key={i}>div {i}</div>)}
<div>{`totalHeight: ${sidebarHeight - formHeight - 30}`}</div>
@@ -427,10 +582,10 @@ export const SidebarChat = () => {
{/* input box */}
<div // this div is used to position the input box properly
className={`right-0 left-0 m-2 z-[999] ${previousMessages.length > 0 ? 'absolute bottom-0' : ''}`}
className={`right-0 left-0 m-2 z-[999] overflow-hidden ${previousMessages.length > 0 ? 'absolute bottom-0' : ''}`}
>
<form
ref={(ref) => { if (ref) { setFormHeight(ref.clientHeight); } }}
ref={formRef}
className={`
flex flex-col gap-2 p-2 relative input text-left shrink-0
transition-all duration-200
@@ -448,9 +603,7 @@ export const SidebarChat = () => {
onSubmit(e)
}}
onClick={(e) => {
if (e.currentTarget === e.target) {
inputBoxRef.current?.focus()
}
inputBoxRef.current?.focus()
}}
>
{/* top row */}
@@ -474,16 +627,22 @@ export const SidebarChat = () => {
{/* middle row */}
<div
className={
// // overwrite vscode styles (generated with this code):
// // 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 input and textarea elements
// .map(style => `@@[&_textarea]:!void-${style}`) // apply styles to ancestor textarea elements
// .join(' ') +
// ` outline-none`
// ` outline-none border-none`
// .split(' ')
// .map(style => `@@[&_div.monaco-inputbox]:!void-${style}`) // apply styles to ancestor input and textarea elements
// .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- @@[&_div.monaco-inputbox]:!void-outline-none`
`@@[&_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`
}
>
@@ -501,34 +660,24 @@ export const SidebarChat = () => {
className='flex flex-row justify-between items-end gap-1'
>
{/* submit options */}
<div className='w-[250px]'>
<div className='max-w-[150px]
@@[&_select]:!void-border-none
@@[&_select]:!void-outline-none'
>
<ModelDropdown featureName='Ctrl+L' />
</div>
{/* submit / stop button */}
{isLoading ?
// stop button
<button
className={`size-[20px] rounded-full bg-white cursor-pointer flex items-center justify-center`}
<ButtonStop
onClick={onAbort}
type='button'
>
<IconSquare size={16} className="stroke-[2]" />
</button>
/>
:
// submit button (up arrow)
<button
className={`size-[20px] rounded-full shrink-0 grow-0 cursor-pointer
${isDisabled ?
'bg-vscode-disabled-fg' // cursor-not-allowed
: 'bg-white' // cursor-pointer
}
`}
<ButtonSubmit
disabled={isDisabled}
type='submit'
>
<IconArrowUp size={20} className="stroke-[2]" />
</button>
/>
}
</div>
@@ -1,10 +1,12 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
/*--------------------------------------------------------------------------------------
* 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 { useService, useThreadsState } from '../util/services.js';
import { useAccessor, useThreadsState } from '../util/services.js';
import { IThreadHistoryService } from '../../../threadHistoryService.js';
import { ISidebarStateService } from '../../../sidebarStateService.js';
const truncate = (s: string) => {
@@ -18,8 +20,10 @@ const truncate = (s: string) => {
export const SidebarThreadSelector = () => {
const threadsState = useThreadsState()
const threadsStateService = useService('threadsStateService')
const sidebarStateService = useService('sidebarStateService')
const accessor = useAccessor()
const threadsStateService = accessor.get('IThreadHistoryService')
const sidebarStateService = accessor.get('ISidebarStateService')
const { allThreads } = threadsState
@@ -1,3 +1,8 @@
/*--------------------------------------------------------------------------------------
* 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,13 +1,26 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
/*--------------------------------------------------------------------------------------
* 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;
@tailwind utilities;
.select-child-restyle select {
text-overflow: ellipsis;
white-space: nowrap;
padding-right: 24px;
}
* {
outline: none !important;
}
/* html {
font-size: var(--vscode-font-size);
}
@@ -1,23 +1,29 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
/*--------------------------------------------------------------------------------------
* 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 { useService } from '../util/services.js';
import { IInputBoxStyles, InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js';
import { defaultInputBoxStyles, defaultSelectBoxStyles } from '../../../../../../../platform/theme/browser/defaultStyles.js';
import { defaultCheckboxStyles, defaultInputBoxStyles, defaultSelectBoxStyles } from '../../../../../../../platform/theme/browser/defaultStyles.js';
import { SelectBox } from '../../../../../../../base/browser/ui/selectBox/selectBox.js';
import { IDisposable } from '../../../../../../../base/common/lifecycle.js';
import { DomScrollableElement } from '../../../../../../../base/browser/ui/scrollbar/scrollableElement.js';
import { ScrollableElementCreationOptions } from '../../../../../../../base/browser/ui/scrollbar/scrollableElementOptions.js';
import { Checkbox } from '../../../../../../../base/browser/ui/toggle/toggle.js';
import { CodeEditorWidget } from '../../../../../../../editor/browser/widget/codeEditor/codeEditorWidget.js'
import { useAccessor } from './services.js';
// type guard
const isConstructor = (f: any)
: f is { new(...params: any[]): any } => {
return !!f.prototype && f.prototype.constructor === f;
}
export const WidgetComponent = <CtorParams extends any[], Instance>({ ctor, propsFn, dispose, onCreateInstance, children, className }
: {
ctor: { new(...params: CtorParams): Instance },
propsFn: (container: HTMLDivElement) => CtorParams,
ctor: { new(...params: CtorParams): Instance } | ((container: HTMLDivElement) => Instance),
propsFn: (container: HTMLDivElement) => CtorParams, // unused if fn
onCreateInstance: (instance: Instance) => IDisposable[],
dispose: (instance: Instance) => void,
children?: React.ReactNode,
@@ -27,7 +33,7 @@ export const WidgetComponent = <CtorParams extends any[], Instance>({ ctor, prop
const containerRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
const instance = new ctor(...propsFn(containerRef.current!));
const instance = isConstructor(ctor) ? new ctor(...propsFn(containerRef.current!)) : ctor(containerRef.current!)
const disposables = onCreateInstance(instance);
return () => {
disposables.forEach(d => d.dispose());
@@ -49,8 +55,9 @@ export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, plac
multiline: boolean;
}) => {
const contextViewProvider = useService('contextViewService');
const accessor = useAccessor()
const contextViewProvider = accessor.get('IContextViewService')
return <WidgetComponent
ctor={InputBox}
propsFn={useCallback((container) => [
@@ -59,6 +66,7 @@ export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, plac
{
inputBoxStyles: {
...defaultInputBoxStyles,
inputForeground: "var(--vscode-foreground)",
// inputBackground: 'transparent',
// inputBorder: 'none',
...styles,
@@ -67,7 +75,7 @@ export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, plac
tooltip: '',
flexibleHeight: multiline,
flexibleMaxHeight: 500,
flexibleWidth: true,
flexibleWidth: false,
}
] as const, [contextViewProvider, placeholder, multiline])}
dispose={useCallback((instance: InputBox) => {
@@ -95,18 +103,109 @@ export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, plac
export const VoidSwitch = ({
value,
onChange,
size = 'md',
label,
disabled = false,
}: {
value: boolean;
onChange: (value: boolean) => void;
label?: string;
disabled?: boolean;
size?: 'xs' | 'sm' | 'sm+' | 'md';
}) => {
return (
<label className="inline-flex items-center cursor-pointer">
<div
onClick={() => !disabled && onChange(!value)}
className={`
relative inline-flex items-center rounded-full transition-colors duration-200 ease-in-out
${value ? 'bg-gray-900 dark:bg-white' : 'bg-gray-200 dark:bg-gray-700'}
${disabled ? 'opacity-25' : ''}
${size === 'xs' ? 'h-4 w-7' : ''}
${size === 'sm' ? 'h-5 w-9' : ''}
${size === 'sm+' ? 'h-5 w-10' : ''}
${size === 'md' ? 'h-6 w-11' : ''}
`}
>
<span
className={`
inline-block transform rounded-full bg-white dark:bg-gray-900 shadow transition-transform duration-200 ease-in-out
${size === 'xs' ? 'h-2.5 w-2.5' : ''}
${size === 'sm' ? 'h-3 w-3' : ''}
${size === 'sm+' ? 'h-3.5 w-3.5' : ''}
${size === 'md' ? 'h-4 w-4' : ''}
${size === 'xs' ? (value ? 'translate-x-3.5' : 'translate-x-0.5') : ''}
${size === 'sm' ? (value ? 'translate-x-5' : 'translate-x-1') : ''}
${size === 'sm+' ? (value ? 'translate-x-6' : 'translate-x-1') : ''}
${size === 'md' ? (value ? 'translate-x-6' : 'translate-x-1') : ''}
`}
/>
</div>
{label && (
<span className={`
ml-3 font-medium text-gray-900 dark:text-gray-100
${size === 'xs' ? 'text-xs' : 'text-sm'}
`}>
{label}
</span>
)}
</label>
);
};
export const VoidCheckBox = ({ label, value, onClick, className }: { label: string, value: boolean, onClick: (checked: boolean) => void, className?: string }) => {
const divRef = useRef<HTMLDivElement | null>(null)
const instanceRef = useRef<Checkbox | null>(null)
useEffect(() => {
if (!instanceRef.current) return
instanceRef.current.checked = value
}, [value])
return <WidgetComponent
className={className ?? ''}
ctor={Checkbox}
propsFn={useCallback((container: HTMLDivElement) => {
divRef.current = container
return [label, value, defaultCheckboxStyles] as const
}, [label, value])}
onCreateInstance={useCallback((instance: Checkbox) => {
instanceRef.current = instance;
divRef.current?.append(instance.domNode)
const d = instance.onChange(() => onClick(instance.checked))
return [d]
}, [onClick])}
dispose={useCallback((instance: Checkbox) => {
instance.dispose()
instance.domNode.remove()
}, [])}
/>
}
export const VoidSelectBox = <T,>({ onChangeSelection, onCreateInstance, selectBoxRef, options }: {
onChangeSelection: (value: T) => void;
onCreateInstance?: ((instance: SelectBox) => void | IDisposable[]);
selectBoxRef?: React.MutableRefObject<SelectBox | null>;
options: readonly { text: string, value: T }[];
}) => {
const contextViewProvider = useService('contextViewService');
const accessor = useAccessor()
const contextViewProvider = accessor.get('IContextViewService')
let containerRef = useRef<HTMLDivElement | null>(null);
return <WidgetComponent
className='text-ellipsis whitespace-nowrap pr-6'
className='@@select-child-restyle'
ctor={SelectBox}
propsFn={useCallback((container) => {
containerRef.current = container
@@ -148,6 +247,147 @@ export const VoidSelectBox = <T,>({ onChangeSelection, onCreateInstance, selectB
/>;
};
// makes it so that code in the sidebar isnt too tabbed out
const normalizeIndentation = (code: string): string => {
const lines = code.split('\n')
let minLeadingSpaces = Infinity
// find the minimum number of leading spaces
for (const line of lines) {
if (line.trim() === '') continue;
let leadingSpaces = 0;
for (let i = 0; i < line.length; i++) {
const char = line[i];
if (char === '\t' || char === ' ') {
leadingSpaces += 1;
} else { break; }
}
minLeadingSpaces = Math.min(minLeadingSpaces, leadingSpaces)
}
// remove the leading spaces
return lines.map(line => {
if (line.trim() === '') return line;
let spacesToRemove = minLeadingSpaces;
let i = 0;
while (spacesToRemove > 0 && i < line.length) {
const char = line[i];
if (char === '\t' || char === ' ') {
spacesToRemove -= 1;
i++;
} else { break; }
}
return line.slice(i);
}).join('\n')
}
export const VoidCodeEditor = ({ initValue, language }: { initValue: string, language: string | undefined }) => {
const MAX_HEIGHT = Infinity;
const divRef = useRef<HTMLDivElement | null>(null)
const accessor = useAccessor()
const instantiationService = accessor.get('IInstantiationService')
const modelService = accessor.get('IModelService')
const languageDetectionService = accessor.get('ILanguageDetectionService')
initValue = normalizeIndentation(initValue)
return <div ref={divRef}>
<WidgetComponent
className='relative z-0 text-sm bg-vscode-editor-bg'
ctor={useCallback((container) =>
instantiationService.createInstance(
CodeEditorWidget,
container,
{
automaticLayout: true,
wordWrap: 'off',
scrollbar: {
alwaysConsumeMouseWheel: false,
vertical: 'hidden',
horizontal: 'hidden',
verticalScrollbarSize: 0,
horizontalScrollbarSize: 0,
},
scrollBeyondLastLine: false,
lineNumbers: 'off',
readOnly: true,
domReadOnly: true,
readOnlyMessage: { value: '' },
minimap: {
enabled: false,
// maxColumn: 0,
},
selectionHighlight: false, // highlights whole words
renderLineHighlight: 'none',
folding: false,
lineDecorationsWidth: 0,
overviewRulerLanes: 0,
hideCursorInOverviewRuler: true,
overviewRulerBorder: false,
glyphMargin: false,
stickyScroll: {
enabled: false,
},
},
{
isSimpleWidget: true,
})
, [instantiationService])
}
onCreateInstance={useCallback((editor: CodeEditorWidget) => {
const model = modelService.createModel(
initValue,
language ? {
languageId: language,
onDidChange: () => ({
dispose: () => { }
})
} : null
);
editor.setModel(model);
const container = editor.getDomNode()
const parentNode = container?.parentElement
const resize = () => {
if (parentNode) {
const height = Math.min(editor.getScrollHeight() + 1, MAX_HEIGHT);
parentNode.style.height = `${height}px`;
editor.layout();
}
}
resize()
const disposable = editor.onDidContentSizeChange(() => { resize() });
return [disposable]
}, [modelService, initValue, language])}
dispose={useCallback((editor: CodeEditorWidget) => {
editor.dispose();
}, [modelService, languageDetectionService])}
propsFn={useCallback(() => { return [] }, [])}
/>
</div>
}
// export const VoidScrollableElt = ({ options, children }: { options: ScrollableElementCreationOptions, children: React.ReactNode }) => {
// const instanceRef = useRef<DomScrollableElement | null>(null);
@@ -182,8 +422,6 @@ export const VoidSelectBox = <T,>({ onChangeSelection, onCreateInstance, selectB
// options: readonly { text: string, value: T }[];
// onChangeSelection: (value: T) => void;
// }) => {
// const contextViewProvider = useService('contextViewService');
// const contextMenuProvider = useService('contextMenuService');
// return <WidgetComponent
@@ -229,9 +467,6 @@ export const VoidSelectBox = <T,>({ onChangeSelection, onCreateInstance, selectB
// }) => {
// const containerRef = useRef<HTMLDivElement>(null);
// const themeService = useService('themeService');
// const contextViewService = useService('contextViewService');
// const hoverService = useService('hoverService');
// useEffect(() => {
// if (!containerRef.current) return;
@@ -1,25 +1,26 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
/*--------------------------------------------------------------------------------------
* 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'
import { _registerServices } from './services.js';
import { ReactServicesType } from '../../../helpers/reactServicesHelper.js';
export const mountFnGenerator = (Component: (params: any) => React.ReactNode) => (rootElement: HTMLElement, services: ReactServicesType) => {
import { ServicesAccessor } from '../../../../../../../editor/browser/editorExtensions.js';
export const mountFnGenerator = (Component: (params: any) => React.ReactNode) => (rootElement: HTMLElement, accessor: ServicesAccessor, props?: any) => {
if (typeof document === 'undefined') {
console.error('index.tsx error: document was undefined')
return
}
const disposables = _registerServices(services)
const disposables = _registerServices(accessor)
const root = ReactDOM.createRoot(rootElement)
root.render(<Component />); // tailwind dark theme indicator
root.render(<Component {...props} />); // tailwind dark theme indicator
return disposables
}
@@ -1,25 +1,55 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { useState, useEffect } from 'react'
import React, { useState, useEffect } from 'react'
import { ThreadsState } from '../../../threadHistoryService.js'
import { SettingsOfProvider } from '../../../../../../../platform/void/common/voidSettingsTypes.js'
import { RefreshModelState } from '../../../../../../../platform/void/common/refreshModelService.js'
import { RefreshableProviderName, SettingsOfProvider } from '../../../../../../../platform/void/common/voidSettingsTypes.js'
import { IDisposable } from '../../../../../../../base/common/lifecycle.js'
import { ReactServicesType } from '../../../helpers/reactServicesHelper.js'
import { VoidSidebarState } from '../../../sidebarStateService.js'
import { VoidSettingsState } from '../../../../../../../platform/void/common/voidSettingsService.js'
import { ColorScheme } from '../../../../../../../platform/theme/common/theme.js'
import { VoidQuickEditState } from '../../../quickEditStateService.js'
import { RefreshModelStateOfProvider } from '../../../../../../../platform/void/common/refreshModelService.js'
import { ServicesAccessor } from '../../../../../../../editor/browser/editorExtensions.js';
import { IModelService } from '../../../../../../../editor/common/services/model.js';
import { IClipboardService } from '../../../../../../../platform/clipboard/common/clipboardService.js';
import { IContextViewService, IContextMenuService } from '../../../../../../../platform/contextview/browser/contextView.js';
import { IFileService } from '../../../../../../../platform/files/common/files.js';
import { IHoverService } from '../../../../../../../platform/hover/browser/hover.js';
import { IThemeService } from '../../../../../../../platform/theme/common/themeService.js';
import { ILLMMessageService } from '../../../../../../../platform/void/common/llmMessageService.js';
import { IRefreshModelService } from '../../../../../../../platform/void/common/refreshModelService.js';
import { IVoidSettingsService } from '../../../../../../../platform/void/common/voidSettingsService.js';
import { IInlineDiffsService } from '../../../inlineDiffsService.js';
import { IQuickEditStateService } from '../../../quickEditStateService.js';
import { ISidebarStateService } from '../../../sidebarStateService.js';
import { IThreadHistoryService } from '../../../threadHistoryService.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'
import { IContextKeyService } from '../../../../../../../platform/contextkey/common/contextkey.js'
import { INotificationService } from '../../../../../../../platform/notification/common/notification.js'
import { IAccessibilityService } from '../../../../../../../platform/accessibility/common/accessibility.js'
import { ILanguageConfigurationService } from '../../../../../../../editor/common/languages/languageConfigurationRegistry.js'
import { ILanguageFeaturesService } from '../../../../../../../editor/common/services/languageFeatures.js'
import { ILanguageDetectionService } from '../../../../../../services/languageDetection/common/languageDetectionWorkerService.js'
// normally to do this you'd use a useEffect that calls .onDidChangeState(), but useEffect mounts too late and misses initial state changes
let services: ReactServicesType
// 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 quickEditState: VoidQuickEditState
const quickEditStateListeners: Set<(s: VoidQuickEditState) => void> = new Set()
let sidebarState: VoidSidebarState
const sidebarStateListeners: Set<(s: VoidSidebarState) => void> = new Set()
@@ -29,8 +59,9 @@ const threadsStateListeners: Set<(s: ThreadsState) => void> = new Set()
let settingsState: VoidSettingsState
const settingsStateListeners: Set<(s: VoidSettingsState) => void> = new Set()
let refreshModelState: RefreshModelState
const refreshModelStateListeners: Set<(s: RefreshModelState) => void> = new Set()
let refreshModelState: RefreshModelStateOfProvider
const refreshModelStateListeners: Set<(s: RefreshModelStateOfProvider) => void> = new Set()
const refreshModelProviderListeners: Set<(p: RefreshableProviderName, s: RefreshModelStateOfProvider) => void> = new Set()
let colorThemeState: ColorScheme
const colorThemeStateListeners: Set<(s: ColorScheme) => void> = new Set()
@@ -38,7 +69,7 @@ const colorThemeStateListeners: Set<(s: ColorScheme) => void> = new Set()
// must call this before you can use any of the hooks below
// this should only be called ONCE! this is the only place you don't need to dispose onDidChange. If you use state.onDidChange anywhere else, make sure to dispose it!
let wasCalled = false
export const _registerServices = (services_: ReactServicesType) => {
export const _registerServices = (accessor: ServicesAccessor) => {
const disposables: IDisposable[] = []
@@ -49,8 +80,26 @@ export const _registerServices = (services_: ReactServicesType) => {
}
wasCalled = true
services = services_
const { sidebarStateService, settingsStateService, threadsStateService, refreshModelService, themeService } = services
_registerAccessor(accessor)
const stateServices = {
quickEditStateService: accessor.get(IQuickEditStateService),
sidebarStateService: accessor.get(ISidebarStateService),
threadsStateService: accessor.get(IThreadHistoryService),
settingsStateService: accessor.get(IVoidSettingsService),
refreshModelService: accessor.get(IRefreshModelService),
themeService: accessor.get(IThemeService),
}
const { sidebarStateService, quickEditStateService, settingsStateService, threadsStateService, refreshModelService, themeService, } = stateServices
quickEditState = quickEditStateService.state
disposables.push(
quickEditStateService.onDidChangeState(() => {
quickEditState = quickEditStateService.state
quickEditStateListeners.forEach(l => l(quickEditState))
})
)
sidebarState = sidebarStateService.state
disposables.push(
@@ -78,9 +127,10 @@ export const _registerServices = (services_: ReactServicesType) => {
refreshModelState = refreshModelService.state
disposables.push(
refreshModelService.onDidChangeState(() => {
refreshModelService.onDidChangeState((providerName) => {
refreshModelState = refreshModelService.state
refreshModelStateListeners.forEach(l => l(refreshModelState))
refreshModelProviderListeners.forEach(l => l(providerName, refreshModelState))
})
)
@@ -96,16 +146,70 @@ export const _registerServices = (services_: ReactServicesType) => {
}
// -- services --
export const useService = <T extends keyof ReactServicesType,>(serviceName: T): ReactServicesType[T] => {
if (services === null) {
throw new Error('useAccessor must be used within an AccessorProvider')
}
return services[serviceName]
const getReactAccessor = (accessor: ServicesAccessor) => {
const reactAccessor = {
IModelService: accessor.get(IModelService),
IClipboardService: accessor.get(IClipboardService),
IContextViewService: accessor.get(IContextViewService),
IContextMenuService: accessor.get(IContextMenuService),
IFileService: accessor.get(IFileService),
IHoverService: accessor.get(IHoverService),
IThemeService: accessor.get(IThemeService),
ILLMMessageService: accessor.get(ILLMMessageService),
IRefreshModelService: accessor.get(IRefreshModelService),
IVoidSettingsService: accessor.get(IVoidSettingsService),
IInlineDiffsService: accessor.get(IInlineDiffsService),
IQuickEditStateService: accessor.get(IQuickEditStateService),
ISidebarStateService: accessor.get(ISidebarStateService),
IThreadHistoryService: accessor.get(IThreadHistoryService),
IInstantiationService: accessor.get(IInstantiationService),
ICodeEditorService: accessor.get(ICodeEditorService),
ICommandService: accessor.get(ICommandService),
IContextKeyService: accessor.get(IContextKeyService),
INotificationService: accessor.get(INotificationService),
IAccessibilityService: accessor.get(IAccessibilityService),
ILanguageConfigurationService: accessor.get(ILanguageConfigurationService),
ILanguageDetectionService: accessor.get(ILanguageDetectionService),
ILanguageFeaturesService: accessor.get(ILanguageFeaturesService),
} as const
return reactAccessor
}
type ReactAccessor = ReturnType<typeof getReactAccessor>
let reactAccessor_: ReactAccessor | null = null
const _registerAccessor = (accessor: ServicesAccessor) => {
const reactAccessor = getReactAccessor(accessor)
reactAccessor_ = reactAccessor
}
// -- services --
export const useAccessor = () => {
if (!reactAccessor_) {
throw new Error(`⚠️ Void useAccessor was called before _registerServices!`)
}
return { get: <S extends keyof ReactAccessor,>(service: S): ReactAccessor[S] => reactAccessor_![service] }
}
// -- state of services --
export const useQuickEditState = () => {
const [s, ss] = useState(quickEditState)
useEffect(() => {
ss(quickEditState)
quickEditStateListeners.add(ss)
return () => { quickEditStateListeners.delete(ss) }
}, [ss])
return s
}
export const useSidebarState = () => {
const [s, ss] = useState(sidebarState)
useEffect(() => {
@@ -148,7 +252,12 @@ export const useRefreshModelState = () => {
}
export const useRefreshModelListener = (listener: (providerName: RefreshableProviderName, s: RefreshModelStateOfProvider) => void) => {
useEffect(() => {
refreshModelProviderListeners.add(listener)
return () => { refreshModelProviderListeners.delete(listener) }
}, [listener])
}
export const useIsDark = () => {
@@ -1,23 +1,38 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
/*--------------------------------------------------------------------------------------
* 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, useRef, useState } from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { FeatureName, featureNames, ModelSelection, modelSelectionsEqual, ProviderName, providerNames } from '../../../../../../../platform/void/common/voidSettingsTypes.js'
import { useSettingsState, useRefreshModelState, useService } from '../util/services.js'
import { useSettingsState, useRefreshModelState, useAccessor } from '../util/services.js'
import { VoidSelectBox } 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 { ModelOption } from '../../../../../../../platform/void/common/voidSettingsService.js'
const ModelSelectBox = ({ featureName }: { featureName: FeatureName }) => {
const voidSettingsService = useService('settingsStateService')
const settingsState = useSettingsState()
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
}
return true
}
const ModelSelectBox = ({ options, featureName }: { options: ModelOption[], featureName: FeatureName }) => {
const accessor = useAccessor()
const voidSettingsService = accessor.get('IVoidSettingsService')
let weChangedText = false
return <VoidSelectBox
options={settingsState._modelOptions}
options={options}
onChangeSelection={useCallback((newVal: ModelSelection) => {
if (weChangedText) return
voidSettingsService.setModelSelectionOfFeature(featureName, newVal)
@@ -39,16 +54,57 @@ const ModelSelectBox = ({ featureName }: { featureName: FeatureName }) => {
/>
}
const MemoizedModelSelectBox = ({ featureName }: { featureName: FeatureName }) => {
const settingsState = useSettingsState()
const oldOptionsRef = useRef<ModelOption[]>([])
const [memoizedOptions, setMemoizedOptions] = useState(oldOptionsRef.current)
useEffect(() => {
const oldOptions = oldOptionsRef.current
const newOptions = settingsState._modelOptions
if (!optionsEqual(oldOptions, newOptions)) {
setMemoizedOptions(newOptions)
}
oldOptionsRef.current = newOptions
}, [settingsState._modelOptions])
return <ModelSelectBox featureName={featureName} options={memoizedOptions} />
}
const DummySelectBox = () => {
return <VoidSelectBox
options={[{ text: 'Please add a model!', value: null }]}
onChangeSelection={() => { }}
/>
const accessor = useAccessor()
const comandService = accessor.get('ICommandService')
const openSettings = () => {
comandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID);
};
return <div
className={`
flex items-center
flex-nowrap text-ellipsis
text-vscode-charts-yellow
hover:brightness-90 transition-all duration-200
cursor-pointer
`}
onClick={openSettings}
>
<IconWarning
size={20}
className='mr-1 brightness-90'
/>
<span>Model required</span>
</div>
// return <VoidSelectBox
// options={[{ text: 'Please add a model!', value: null }]}
// onChangeSelection={() => { }}
// />
}
export const ModelDropdown = ({ featureName }: { featureName: FeatureName }) => {
const settingsState = useSettingsState()
return <>
{settingsState._modelOptions.length === 0 ? <DummySelectBox /> : <ModelSelectBox featureName={featureName} />}
{settingsState._modelOptions.length === 0 ? <DummySelectBox /> : <MemoizedModelSelectBox featureName={featureName} />}
</>
}
@@ -1,34 +1,87 @@
/*--------------------------------------------------------------------------------------
* 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, titleOfProviderName, providerNames, ModelInfo } from '../../../../../../../platform/void/common/voidSettingsTypes.js'
import { ProviderName, SettingName, displayInfoOfSettingName, providerNames, VoidModelInfo, featureFlagNames, displayInfoOfFeatureFlag, customSettingNamesOfProvider, RefreshableProviderName, refreshableProviderNames, displayInfoOfProviderName, defaultProviderSettings, nonlocalProviderNames, localProviderNames } from '../../../../../../../platform/void/common/voidSettingsTypes.js'
import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js'
import { VoidInputBox, VoidSelectBox } from '../util/inputs.js'
import { useIsDark, useRefreshModelState, useService, useSettingsState } from '../util/services.js'
import { X } from 'lucide-react'
import { VoidCheckBox, VoidInputBox, VoidSelectBox, VoidSwitch } from '../util/inputs.js'
import { useAccessor, useIsDark, useRefreshModelListener, useRefreshModelState, useSettingsState } from '../util/services.js'
import { X, RefreshCw, Loader2, Check } from 'lucide-react'
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'>
<button className='flex items-center' disabled={disabled} onClick={onClick}>
{icon}
</button>
<span className='opacity-50'>
{text}
</span>
</div>
}
// models
const RefreshModelButton = ({ providerName }: { providerName: RefreshableProviderName }) => {
const refreshModelState = useRefreshModelState()
const accessor = useAccessor()
const refreshModelService = accessor.get('IRefreshModelService')
const [justFinished, setJustFinished] = useState(false)
useRefreshModelListener(
useCallback((providerName2, refreshModelState) => {
if (providerName2 !== providerName) return
const { state } = refreshModelState[providerName]
if (state !== 'finished') return
// now we know we just entered 'finished' state for this providerName
setJustFinished(true)
const tid = setTimeout(() => { setJustFinished(false) }, 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}
/>
}
const RefreshableModels = () => {
const settingsState = useSettingsState()
const refreshModelState = useRefreshModelState()
const refreshModelService = useService('refreshModelService')
if (!settingsState.settingsOfProvider.ollama.enabled)
return null
const buttons = refreshableProviderNames.map(providerName => {
if (!settingsState.settingsOfProvider[providerName]._enabled) return null
return <div key={providerName} className='pb-4' >
<RefreshModelButton providerName={providerName} />
</div>
})
return <>
{buttons}
</>
return <div>
<button onClick={() => refreshModelService.refreshOllamaModels()}>refresh Ollama built-in models</button>
{refreshModelState === 'loading' ? 'loading...' : 'good!'}
</div>
}
const AddModelMenu = ({ onSubmit }: { onSubmit: () => void }) => {
const settingsStateService = useService('settingsStateService')
const accessor = useAccessor()
const settingsStateService = accessor.get('IVoidSettingsService')
const settingsState = useSettingsState()
const providerNameRef = useRef<ProviderName | null>(null)
@@ -36,21 +89,14 @@ const AddModelMenu = ({ onSubmit }: { onSubmit: () => void }) => {
const [errorString, setErrorString] = useState('')
const providerOptions = useMemo(() => providerNames.map(providerName => ({ text: titleOfProviderName(providerName), value: providerName })), [providerNames])
const providerOptions = useMemo(() => providerNames.map(providerName => ({ text: displayInfoOfProviderName(providerName).title, value: providerName })), [providerNames])
return <>
<div className='flex justify-center items-center gap-4'>
{/* model */}
<div className='max-w-40 w-full'>
<VoidInputBox
placeholder='Model Name'
onChangeText={useCallback((modelName) => { modelNameRef.current = modelName }, [])}
multiline={false}
/>
</div>
<div className='flex items-center gap-4'>
{/* provider */}
<div className='max-w-40 w-full'>
<div className='max-w-40 w-full border border-vscode-editorwidget-border'>
<VoidSelectBox
onCreateInstance={useCallback(() => { providerNameRef.current = providerOptions[0].value }, [providerOptions])} // initialize state
onChangeSelection={useCallback((providerName: ProviderName) => { providerNameRef.current = providerName }, [])}
@@ -58,9 +104,19 @@ const AddModelMenu = ({ onSubmit }: { onSubmit: () => void }) => {
/>
</div>
{/* model */}
<div className='max-w-40 w-full border border-vscode-editorwidget-border'>
<VoidInputBox
placeholder='Model Name'
onChangeText={useCallback((modelName) => { modelNameRef.current = modelName }, [])}
multiline={false}
/>
</div>
{/* button */}
<div className='max-w-40 w-full'>
<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
@@ -84,54 +140,80 @@ const AddModelMenu = ({ onSubmit }: { onSubmit: () => void }) => {
}}>Add model</button>
</div>
{!errorString ? null : <div className='text-red-500 truncate whitespace-nowrap'>
{errorString}
</div>}
</div>
{!errorString ? null : <div className='text-center text-red-500'>
{errorString}
</div>}
</>
}
const AddModelButton = () => {
const AddModelMenuFull = () => {
const [open, setOpen] = useState(false)
return <>
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 onClick={() => setOpen(true)}>Add Model</button>
: <button
className='px-3 py-1 bg-black/10 dark:bg-gray-200/10 rounded-sm overflow-hidden'
onClick={() => setOpen(true)}
>Add Model</button>
}
</>
</div>
}
export const ModelDump = () => {
const settingsStateService = useService('settingsStateService')
const accessor = useAccessor()
const settingsStateService = accessor.get('IVoidSettingsService')
const settingsState = useSettingsState()
// a dump of all the enabled providers' models
const modelDump: (ModelInfo & { providerName: ProviderName, providerEnabled: boolean })[] = []
const modelDump: (VoidModelInfo & { providerName: ProviderName, providerEnabled: boolean })[] = []
for (let providerName of providerNames) {
const providerSettings = settingsState.settingsOfProvider[providerName]
// if (!providerSettings.enabled) continue
modelDump.push(...providerSettings.models.map(model => ({ ...model, providerName, providerEnabled: providerSettings.enabled })))
modelDump.push(...providerSettings.models.map(model => ({ ...model, providerName, providerEnabled: !!providerSettings._enabled })))
}
return <div className=''>
{modelDump.map(m => {
const { isHidden, isDefault, modelName, providerName, providerEnabled } = m
// sort by hidden
modelDump.sort((a, b) => {
return Number(b.providerEnabled) - Number(a.providerEnabled)
})
return <div key={`${modelName}${providerName}`} className='flex items-center justify-between gap-4 hover:bg-black/10 dark:hover:bg-gray-200/10 py-1 px-3 rounded-sm overflow-hidden cursor-default'>
return <div className=''>
{modelDump.map((m, i) => {
const { isHidden, isDefault, isAutodetected, modelName, providerName, providerEnabled } = m
const isNewProviderName = (i > 0 ? modelDump[i - 1] : undefined)?.providerName !== providerName
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' : ''}`}>
{/* left part is width:full */}
<div className='w-full flex items-center gap-4'>
<span>{`${modelName} (${providerName})`}</span>
<div className={`w-full flex items-center gap-4`}>
<span className='min-w-40'>{isNewProviderName ? displayInfoOfProviderName(providerName).title : ''}</span>
<span>{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 whitespace-nowrap'>{isDefault ? '' : '(custom model)'}</span>
<button disabled={!providerEnabled} onClick={() => { settingsStateService.toggleModelHidden(providerName, modelName) }}>{(!providerEnabled || isHidden) ? '❌' : '✅'}</button>
<div className='w-5 flex items-center justify-center'>
<span className='opacity-50'>{isAutodetected ? '(detected locally)' : isDefault ? '' : '(custom model)'}</span>
<VoidSwitch
value={disabled ? false : !isHidden}
onChange={() => { settingsStateService.toggleModelHidden(providerName, modelName) }}
disabled={disabled}
size='sm'
/>
<div className={`w-5 flex items-center justify-center`}>
{isDefault ? null : <button onClick={() => { settingsStateService.deleteModel(providerName, modelName) }}><X className='size-4' /></button>}
</div>
</div>
@@ -146,63 +228,105 @@ export const ModelDump = () => {
const ProviderSetting = ({ providerName, settingName }: { providerName: ProviderName, settingName: SettingName }) => {
const { title, placeholder } = displayInfoOfSettingName(providerName, settingName)
const voidSettingsService = useService('settingsStateService')
// const { title: providerTitle, } = displayInfoOfProviderName(providerName)
const { title: settingTitle, placeholder, subTextMd } = displayInfoOfSettingName(providerName, settingName)
const accessor = useAccessor()
const voidSettingsService = accessor.get('IVoidSettingsService')
let weChangedTextRef = false
return <><ErrorBoundary>
<label>{title}</label>
<VoidInputBox
placeholder={placeholder}
onChangeText={useCallback((newVal) => {
if (weChangedTextRef) return
voidSettingsService.setSettingOfProvider(providerName, settingName, newVal)
}, [voidSettingsService, providerName, settingName])}
return <ErrorBoundary>
<div className='my-1'>
<VoidInputBox
// placeholder={`${providerTitle} ${settingTitle} (${placeholder}).`}
placeholder={`${settingTitle} (${placeholder}).`}
onChangeText={useCallback((newVal) => {
if (weChangedTextRef) return
voidSettingsService.setSettingOfProvider(providerName, settingName, newVal)
}, [voidSettingsService, providerName, settingName])}
// we are responsible for setting the initial value. always sync the instance whenever there's a change to state.
onCreateInstance={useCallback((instance: InputBox) => {
const syncInstance = () => {
const settingsAtProvider = voidSettingsService.state.settingsOfProvider[providerName];
const stateVal = settingsAtProvider[settingName as SettingName]
// console.log('SYNCING TO', providerName, settingName, stateVal)
weChangedTextRef = true
instance.value = stateVal as string
weChangedTextRef = false
}
syncInstance()
const disposable = voidSettingsService.onDidChangeState(syncInstance)
return [disposable]
}, [voidSettingsService, providerName, settingName])}
multiline={false}
/>
</ErrorBoundary></>
// we are responsible for setting the initial value. always sync the instance whenever there's a change to state.
onCreateInstance={useCallback((instance: InputBox) => {
const syncInstance = () => {
const settingsAtProvider = voidSettingsService.state.settingsOfProvider[providerName];
const stateVal = settingsAtProvider[settingName as SettingName]
// console.log('SYNCING TO', providerName, settingName, stateVal)
weChangedTextRef = true
instance.value = stateVal as string
weChangedTextRef = false
const isEverySettingPresent = Object.keys(defaultProviderSettings[providerName]).every(key => {
return !!settingsAtProvider[key as keyof typeof settingsAtProvider]
})
const shouldEnable = isEverySettingPresent && !settingsAtProvider._enabled // enable if all settings are present and not already enabled
const shouldDisable = !isEverySettingPresent && settingsAtProvider._enabled
if (shouldEnable) {
voidSettingsService.setSettingOfProvider(providerName, '_enabled', true)
}
if (shouldDisable) {
voidSettingsService.setSettingOfProvider(providerName, '_enabled', false)
}
}
syncInstance()
const disposable = voidSettingsService.onDidChangeState(syncInstance)
return [disposable]
}, [voidSettingsService, providerName, settingName])}
multiline={false}
/>
{subTextMd === undefined ? null : <div className='py-1 px-3 opacity-50 text-xs'>
<ChatMarkdownRender string={subTextMd} />
</div>}
</div>
</ErrorBoundary>
}
const SettingsForProvider = ({ providerName }: { providerName: ProviderName }) => {
const voidSettingsState = useSettingsState()
const voidSettingsService = useService('settingsStateService')
// const voidSettingsState = useSettingsState()
// const accessor = useAccessor()
// const voidSettingsService = accessor.get('IVoidSettingsService')
const { models, enabled, ...others } = voidSettingsState.settingsOfProvider[providerName]
// const { enabled } = voidSettingsState.settingsOfProvider[providerName]
const settingNames = customSettingNamesOfProvider(providerName)
return <>
const { title: providerTitle } = displayInfoOfProviderName(providerName)
<div className='flex items-center gap-4'>
<h3 className='text-xl'>{titleOfProviderName(providerName)}</h3>
<button onClick={() => { voidSettingsService.setSettingOfProvider(providerName, 'enabled', !enabled) }}>{enabled ? '✅' : '❌'}</button>
return <div className='my-4'>
<div className='flex items-center w-full gap-4'>
<h3 className='text-xl truncate'>{providerTitle}</h3>
{/* enable provider switch */}
{/* <VoidSwitch
value={!!enabled}
onChange={
useCallback(() => {
const enabledRef = voidSettingsService.state.settingsOfProvider[providerName].enabled
voidSettingsService.setSettingOfProvider(providerName, 'enabled', !enabledRef)
}, [voidSettingsService, providerName])}
size='sm+'
/> */}
</div>
{/* settings besides models (e.g. api key) */}
{Object.keys(others).map((sName, i) => {
const settingName = sName as keyof typeof others
return <ProviderSetting key={settingName} providerName={providerName} settingName={settingName} />
})}
</>
<div className='px-0'>
{/* settings besides models (e.g. api key) */}
{settingNames.map((settingName, i) => {
return <ProviderSetting key={settingName} providerName={providerName} settingName={settingName} />
})}
</div>
</div >
}
export const VoidProviderSettings = () => {
export const VoidProviderSettings = ({ providerNames }: { providerNames: ProviderName[] }) => {
return <>
{providerNames.map(providerName =>
<SettingsForProvider key={providerName} providerName={providerName} />
@@ -210,6 +334,50 @@ export const VoidProviderSettings = () => {
</>
}
// 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 = () => {
const accessor = useAccessor()
const voidSettingsService = accessor.get('IVoidSettingsService')
const voidSettingsState = useSettingsState()
return featureFlagNames.map((flagName) => {
// 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}
/>
})
}
// full settings
@@ -233,12 +401,12 @@ export const Settings = () => {
{/* tabs */}
<div className='flex flex-col w-full max-w-32'>
<button className={`text-left p-1 my-0.5 rounded-sm overflow-hidden ${tab === 'models' ? 'bg-vscode-button-hover-bg' : 'bg-vscode-button-active-bg'} hover:bg-vscode-button-hover-bg active:bg-vscode-button-active-bg`}
<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 my-0.5 rounded-sm overflow-hidden ${tab === 'features' ? 'bg-vscode-button-hover-bg' : 'bg-vscode-button-active-bg'} hover:bg-vscode-button-hover-bg active:bg-vscode-button-active-bg`}
{/* <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>
>Features</button> */}
</div>
{/* separator */}
@@ -249,19 +417,41 @@ export const Settings = () => {
<div className='w-full overflow-y-auto'>
<div className={`${tab !== 'models' ? 'hidden' : ''}`}>
<h2 className={`text-3xl mb-2`}>Models</h2>
<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>
<ModelDump />
<AddModelButton />
<RefreshableModels />
<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 />
<VoidProviderSettings providerNames={nonlocalProviderNames} />
</ErrorBoundary>
<h2 className={`text-3xl mb-2 mt-16`}>Models</h2>
<ErrorBoundary>
<VoidFeatureFlagSettings />
<RefreshableModels />
<ModelDump />
<AddModelMenuFull />
</ErrorBoundary>
</div>
<div className={`${tab !== 'features' ? 'hidden' : ''}`}>
<h2 className={`text-3xl mb-2`} onClick={() => { setTab('features') }}>Features</h2>
{/* <VoidFeatureFlagSettings /> */}
</div>
</div>
@@ -1,3 +1,8 @@
/*--------------------------------------------------------------------------------------
* 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) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
/*--------------------------------------------------------------------------------------
* 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 = {
@@ -41,18 +41,18 @@ module.exports = {
"input-fg": "var(--vscode-input-foreground)",
"input-placeholder-fg": "var(--vscode-placeholderForeground)",
"input-active-bg": "var(--vscode-activeBackground)",
"input-option-active-border": "var(--vscode-activeBorder)",
"input-option-active-fg": "var(--vscode-activeForeground)",
"input-option-hover-bg": "var(--vscode-hoverBackground)",
"input-validation-error-bg": "var(--vscode-errorBackground)",
"input-validation-error-fg": "var(--vscode-errorForeground)",
"input-validation-error-border": "var(--vscode-errorBorder)",
"input-validation-info-bg": "var(--vscode-infoBackground)",
"input-validation-info-fg": "var(--vscode-infoForeground)",
"input-validation-info-border": "var(--vscode-infoBorder)",
"input-validation-warning-bg": "var(--vscode-warningBackground)",
"input-validation-warning-fg": "var(--vscode-warningForeground)",
"input-validation-warning-border": "var(--vscode-warningBorder)",
"input-option-active-border": "var(--vscode-inputOption-activeBorder)",
"input-option-active-fg": "var(--vscode-inputOption-activeForeground)",
"input-option-hover-bg": "var(--vscode-inputOption-hoverBackground)",
"input-validation-error-bg": "var(--vscode-inputValidation-errorBackground)",
"input-validation-error-fg": "var(--vscode-inputValidation-errorForeground)",
"input-validation-error-border": "var(--vscode-inputValidation-errorBorder)",
"input-validation-info-bg": "var(--vscode-inputValidation-infoBackground)",
"input-validation-info-fg": "var(--vscode-inputValidation-infoForeground)",
"input-validation-info-border": "var(--vscode-inputValidation-infoBorder)",
"input-validation-warning-bg": "var(--vscode-inputValidation-warningBackground)",
"input-validation-warning-fg": "var(--vscode-inputValidation-warningForeground)",
"input-validation-warning-border": "var(--vscode-inputValidation-warningBorder)",
// command center colors (the top bar)
"commandcenter-fg": "var(--vscode-commandCenter-foreground)",
@@ -89,7 +89,7 @@ module.exports = {
"sidebar-bg": "var(--vscode-sideBar-background)",
"sidebar-fg": "var(--vscode-sideBar-foreground)",
"sidebar-border": "var(--vscode-sideBar-border)",
"sidebar-drop-backdrop": "var(--vscode-sideBar-dropBackground)",
"sidebar-drop-bg": "var(--vscode-sideBar-dropBackground)",
"sidebar-title-fg": "var(--vscode-sideBarTitle-foreground)",
"sidebar-header-bg": "var(--vscode-sideBarSectionHeader-background)",
"sidebar-header-fg": "var(--vscode-sideBarSectionHeader-foreground)",
@@ -103,15 +103,26 @@ module.exports = {
// other colors (these are partially complete)
// text formatting
"text-preformat-bg": "var(--vscode-textPreformat-background)",
"text-preformat-fg": "var(--vscode-textPreformat-foreground)",
// editor colors
"editor-bg": "var(--vscode-editor-background)",
"editor-fg": "var(--vscode-editor-foreground)",
// editorwidget colors
"editorwidget-fg": "var(--vscode-editorWidget-foreground)",
// other
"editorwidget-bg": "var(--vscode-editorWidget-background)",
"toolbar-hover-bg": "var(--vscode-toolbar-hoverBackground)",
"toolbar-foreground": "var(--vscode-editorActionList-foreground)",
"editorwidget-fg": "var(--vscode-editorWidget-foreground)",
"editorwidget-border": "var(--vscode-editorWidget-border)",
"charts-orange": "var(--vscode-charts-orange)",
"charts-yellow": "var(--vscode-charts-yellow)",
},
},
},
@@ -1,7 +1,7 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
{
"compilerOptions": {
@@ -1,7 +1,7 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
/*--------------------------------------------------------------------------------------
* 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,6 +9,7 @@ export default defineConfig({
entry: [
'./src2/sidebar-tsx/index.tsx',
'./src2/void-settings-tsx/index.tsx',
'./src2/ctrl-k-tsx/index.tsx',
'./src2/diff/index.tsx',
],
outDir: './out',
@@ -18,7 +19,7 @@ export default defineConfig({
// dts: true,
// sourcemap: true,
clean: true,
clean: false,
platform: 'browser', // 'node'
target: 'esnext',
injectStyle: true, // bundle css into the output file
@@ -1,7 +1,7 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
/*--------------------------------------------------------------------------------------
* 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';
@@ -12,9 +12,7 @@ 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 { IEditorService } from '../../../services/editor/common/editorService.js';
import { IEditorService } from '../../../services/editor/common/editorService.js';
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
import { IRange } from '../../../../editor/common/core/range.js';
import { ITextModel } from '../../../../editor/common/model.js';
@@ -22,7 +20,7 @@ import { 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 { OPEN_VOID_SETTINGS_ACTION_ID } from './voidSettingsPane.js';
import { VOID_OPEN_SETTINGS_ACTION_ID } from './voidSettingsPane.js';
// ---------- Register commands and keybindings ----------
@@ -53,9 +51,10 @@ const getContentInRange = (model: ITextModel, range: IRange | null) => {
}
// Action: when press ctrl+L, show the sidebar chat and add to the selection
export const VOID_CTRL_L_ACTION_ID = 'void.ctrlLAction'
registerAction2(class extends Action2 {
constructor() {
super({ id: 'void.ctrl+l', title: 'Show Sidebar', keybinding: { primary: KeyMod.CtrlCmd | KeyCode.KeyL, weight: KeybindingWeight.BuiltinExtension } });
super({ id: VOID_CTRL_L_ACTION_ID, title: 'Void: Show Sidebar', keybinding: { primary: KeyMod.CtrlCmd | KeyCode.KeyL, weight: KeybindingWeight.BuiltinExtension } });
}
async run(accessor: ServicesAccessor): Promise<void> {
@@ -65,18 +64,23 @@ registerAction2(class extends Action2 {
const stateService = accessor.get(ISidebarStateService)
const metricsService = accessor.get(IMetricsService)
const editorService = accessor.get(ICodeEditorService)
metricsService.capture('Chat Navigation', { type: 'Ctrl+L' })
metricsService.capture('User Action', { type: 'Ctrl+L' })
stateService.setState({ isHistoryOpen: false, currentTab: 'chat' })
stateService.fireFocusChat()
const editor = editorService.getActiveCodeEditor()
const selectionRange = roundRangeToLines(
accessor.get(IEditorService).activeTextEditorControl?.getSelection()
// accessor.get(IEditorService).activeTextEditorControl?.getSelection()
editor?.getSelection()
)
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)
@@ -176,6 +180,6 @@ registerAction2(class extends Action2 {
}
async run(accessor: ServicesAccessor): Promise<void> {
const commandService = accessor.get(ICommandService)
commandService.executeCommand(OPEN_VOID_SETTINGS_ACTION_ID)
commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID)
}
})
@@ -1,7 +1,7 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
/*--------------------------------------------------------------------------------------
* 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 {
@@ -25,7 +25,7 @@ import { IViewPaneOptions, ViewPane } from '../../../browser/parts/views/viewPan
import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
import { IDisposable } from '../../../../base/common/lifecycle.js';
// import { IDisposable } from '../../../../base/common/lifecycle.js';
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';
@@ -33,16 +33,17 @@ import { IKeybindingService } from '../../../../platform/keybinding/common/keybi
import { IOpenerService } from '../../../../platform/opener/common/opener.js';
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
import { IHoverService } from '../../../../platform/hover/browser/hover.js';
import { mountSidebar } from './react/out/sidebar-tsx/index.js';
import { getReactServices } from './helpers/reactServicesHelper.js';
import { Codicon } from '../../../../base/common/codicons.js';
import { Orientation } from '../../../../base/browser/ui/sash/sash.js';
// import { Orientation } from '../../../../base/browser/ui/sash/sash.js';
// import { Codicon } from '../../../../base/common/codicons.js';
// import { Codicon } from '../../../../base/common/codicons.js';
// import { IDisposable } from '../../../../base/common/lifecycle.js';
import { IDisposable } from '../../../../base/common/lifecycle.js';
import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js';
import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js';
import { IViewsService } from '../../../services/views/common/viewsService.js';
import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js';
import { ICommandService } from '../../../../platform/commands/common/commands.js';
// compare against search.contribution.ts and debug.contribution.ts, scm.contribution.ts (source control)
@@ -62,6 +63,8 @@ class SidebarViewPane extends ViewPane {
@IOpenerService openerService: IOpenerService,
@ITelemetryService telemetryService: ITelemetryService,
@IHoverService hoverService: IHoverService,
// @ICodeEditorService private readonly editorService: ICodeEditorService,
// @IContextKeyService private readonly editorContextKeyService: IContextKeyService,
) {
super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService, hoverService)
@@ -76,20 +79,16 @@ class SidebarViewPane extends ViewPane {
// gets set immediately
this.instantiationService.invokeFunction(accessor => {
const services = getReactServices(accessor)
// mount react
const disposables: IDisposable[] | undefined = mountSidebar(parent, services);
const disposables: IDisposable[] | undefined = mountSidebar(parent, accessor);
disposables?.forEach(d => this._register(d))
});
}
override layoutBody(height: number, width: number): void {
protected override layoutBody(height: number, width: number): void {
super.layoutBody(height, width)
this.element.style.height = `${height}px`
this.element.style.width = `${width}px`
}
}
@@ -148,3 +147,28 @@ viewsRegistry.registerViews([{
// },
}], container);
// open sidebar
export const VOID_OPEN_SIDEBAR_ACTION_ID = 'void.openSidebar'
registerAction2(class extends Action2 {
constructor() {
super({
id: VOID_OPEN_SIDEBAR_ACTION_ID,
title: 'Open Void Sidebar',
})
}
run(accessor: ServicesAccessor): void {
const viewsService = accessor.get(IViewsService)
viewsService.openViewContainer(VOID_VIEW_CONTAINER_ID);
}
});
export class SidebarStartContribution implements IWorkbenchContribution {
static readonly ID = 'workbench.contrib.startupVoidSidebar';
constructor(
@ICommandService private readonly commandService: ICommandService,
) {
this.commandService.executeCommand(VOID_OPEN_SIDEBAR_ACTION_ID)
}
}
registerWorkbenchContribution2(SidebarStartContribution.ID, SidebarStartContribution, WorkbenchPhase.AfterRestored);
@@ -1,9 +1,14 @@
/*--------------------------------------------------------------------------------------
* 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';
import { ICommandService } from '../../../../platform/commands/common/commands.js';
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { IViewsService } from '../../../services/views/common/viewsService.js';
import { VOID_VIEW_CONTAINER_ID, VOID_VIEW_ID } from './sidebarPane.js';
import { VOID_OPEN_SIDEBAR_ACTION_ID } from './sidebarPane.js';
// service that manages sidebar's state
@@ -23,8 +28,6 @@ export interface ISidebarStateService {
onDidBlurChat: Event<void>;
fireFocusChat(): void;
fireBlurChat(): void;
openSidebarView(): void;
}
export const ISidebarStateService = createDecorator<ISidebarStateService>('voidSidebarStateService');
@@ -47,7 +50,7 @@ class VoidSidebarStateService extends Disposable implements ISidebarStateService
state: VoidSidebarState
constructor(
@IViewsService private readonly _viewsService: IViewsService,
@ICommandService private readonly commandService: ICommandService,
) {
super()
@@ -59,7 +62,7 @@ class VoidSidebarStateService extends Disposable implements ISidebarStateService
setState(newState: Partial<VoidSidebarState>) {
// make sure view is open if the tab changes
if ('currentTab' in newState) {
this.openSidebarView()
this.commandService.executeCommand(VOID_OPEN_SIDEBAR_ACTION_ID)
}
this.state = { ...this.state, ...newState }
@@ -74,11 +77,6 @@ class VoidSidebarStateService extends Disposable implements ISidebarStateService
this._onBlurChat.fire()
}
openSidebarView() {
this._viewsService.openViewContainer(VOID_VIEW_CONTAINER_ID);
this._viewsService.openView(VOID_VIEW_ID);
}
}
registerSingleton(ISidebarStateService, VoidSidebarStateService, InstantiationType.Eager);
@@ -1,7 +1,7 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
/*--------------------------------------------------------------------------------------
* 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';
@@ -1,17 +1,20 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
/*--------------------------------------------------------------------------------------
* 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
import './inlineDiffsService.js'
// register Sidebar pane, state, actions (keybinds, menus)
// register Sidebar pane, state, actions (keybinds, menus) (Ctrl+L)
import './sidebarActions.js'
import './sidebarPane.js'
import './sidebarStateService.js'
// register quick edit (Ctrl+K)
import './quickEditActions.js'
// register Thread History
import './threadHistoryService.js'
@@ -1,7 +1,7 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
import { EditorInput } from '../../../common/editor/editorInput.js';
@@ -24,7 +24,6 @@ import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextke
import { mountVoidSettings } from './react/out/void-settings-tsx/index.js'
import { getReactServices } from './helpers/reactServicesHelper.js';
import { Codicon } from '../../../../base/common/codicons.js';
import { IDisposable } from '../../../../base/common/lifecycle.js';
import { DomScrollableElement } from '../../../../base/browser/ui/scrollbar/scrollableElement.js';
@@ -36,10 +35,11 @@ class VoidSettingsInput extends EditorInput {
static readonly ID: string = 'workbench.input.void.settings';
readonly resource = URI.from({
scheme: 'void-editor-settings',
path: 'void-settings' // Give it a unique path
});
static readonly RESOURCE = URI.from({ // I think this scheme is invalid, it just shuts up TS
scheme: 'void', // Custom scheme for our editor
path: 'settings'
})
readonly resource = VoidSettingsInput.RESOURCE;
constructor() {
super();
@@ -89,8 +89,7 @@ class VoidSettingsPane extends EditorPane {
// Mount React into the scrollable content
this.instantiationService.invokeFunction(accessor => {
const services = getReactServices(accessor);
const disposables: IDisposable[] | undefined = mountVoidSettings(scrollableContent, services);
const disposables: IDisposable[] | undefined = mountVoidSettings(scrollableContent, accessor);
setTimeout(() => { // this is a complete hack and I don't really understand how scrollbar works here
this._scrollbar?.scanDomNode();
@@ -120,12 +119,12 @@ Registry.as<IEditorPaneRegistry>(EditorExtensions.EditorPane).registerEditorPane
);
export const OPEN_VOID_SETTINGS_ACTION_ID = 'workbench.action.openVoidSettings'
export const VOID_OPEN_SETTINGS_ACTION_ID = 'workbench.action.openVoidSettings'
// register the gear on the top right
registerAction2(class extends Action2 {
constructor() {
super({
id: OPEN_VOID_SETTINGS_ACTION_ID,
id: VOID_OPEN_SETTINGS_ACTION_ID,
title: nls.localize2('voidSettings', "Void: Settings"),
f1: true,
icon: Codicon.settingsGear,
@@ -142,9 +141,20 @@ registerAction2(class extends Action2 {
]
});
}
async run(accessor: ServicesAccessor): Promise<void> {
const editorService = accessor.get(IEditorService);
const instantiationService = accessor.get(IInstantiationService);
const openEditors = editorService.findEditors(VoidSettingsInput.RESOURCE);
// close all instances if found
if (openEditors.length > 0) {
await editorService.closeEditors(openEditors);
return;
}
// else open it
const input = instantiationService.createInstance(VoidSettingsInput);
await editorService.openEditor(input);
}
@@ -155,7 +165,7 @@ registerAction2(class extends Action2 {
MenuRegistry.appendMenuItem(MenuId.GlobalActivity, {
group: '0_command',
command: {
id: OPEN_VOID_SETTINGS_ACTION_ID,
id: VOID_OPEN_SETTINGS_ACTION_ID,
title: nls.localize('voidSettings', "Void Settings")
},
order: 1