Compare commits

..

1 Commits

Author SHA1 Message Date
Andrew Pareles ab54f6d097 todesktop requirements 2025-01-03 14:14:54 -08:00
134 changed files with 4748 additions and 9582 deletions
-2
View File
@@ -22,5 +22,3 @@ product.overrides.json
*.snap.actual
.vscode-test
.tmp/
.tmp2/
.tool-versions
+1 -30
View File
@@ -29,34 +29,6 @@
}
}
},
{
"type": "npm",
"script": "watchreactd",
"label": "React - Build",
"isBackground": true,
"presentation": {
"reveal": "never",
"group": "buildWatchers",
"close": false
},
"problemMatcher": {
"owner": "typescript",
"applyTo": "closedDocuments",
"fileLocation": [
"absolute"
],
"pattern": {
"regexp": "Error: ([^(]+)\\((\\d+|\\d+,\\d+|\\d+,\\d+,\\d+,\\d+)\\): (.*)$",
"file": 1,
"location": 2,
"message": 3
},
"background": {
"beginsPattern": "Starting compilation",
"endsPattern": "Finished compilation"
}
}
},
{
"type": "npm",
"script": "watch-extensionsd",
@@ -89,8 +61,7 @@
"label": "VS Code - Build",
"dependsOn": [
"Core - Build",
"Ext - Build",
"React - Build"
"Ext - Build"
],
"group": {
"kind": "build",
+38 -51
View File
@@ -1,29 +1,18 @@
# Contributing to Void
### Welcome! 👋
This is the official guide on how to contribute to Void. We want to make it as easy as possible to contribute, so if you have any questions or comments, reach out via email or discord!
Welcome! 👋 This is the official guide on how to contribute to Void. We want to make it as easy as possible to contribute, so if you have any questions or comments, reach out via email or discord!
There are a few ways to contribute:
- 💫 Complete items on the [Roadmap](https://github.com/orgs/voideditor/projects/2).
- 👨‍💻 Build new features - see [Issues](https://github.com/voideditor/void/issues).
- 💡 Make suggestions in our [Discord](https://discord.gg/RSNjgaugJs).
- 🪴 Start new Issues - see [Issues](https://github.com/voideditor/void/issues).
- ⭐️ If you want to build your AI tool into Void, feel free to get in touch! It's very easy to extend Void, and the UX you create will be much more natural than a VSCode Extension.
Void's code lives in `src/vs/workbench/contrib/void/browser/` and `src/vs/platform/void/`.
### Codebase Guide
We highly recommend reading [this](https://github.com/microsoft/vscode/wiki/Source-Code-Organization) article on VSCode's sourcecode organization.
<!-- ADD BLOG HERE
We wrote a [guide to working in VSCode].
-->
Most of Void's code lives in the two folders called `void/`.
## Building Void
## Building the full IDE
### a. Build Prerequisites - Mac
@@ -52,22 +41,21 @@ First, run `npm install -g node-gyp`. Then:
- Red Hat (Fedora, etc): `sudo dnf install @development-tools gcc gcc-c++ make libsecret-devel krb5-devel libX11-devel libxkbfile-devel`.
- Others: see [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute).
### d. Building Void
### Building Void
To build Void, open `void/` inside VSCode. Then open your terminal and run:
1. `npm install` to install all dependencies.
2. `npm run watchreact` to build Void's browser dependencies like React. (If this doesn't work, try `npm run buildreact`).
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 Void.
- Run `./scripts/code.sh` (Mac/Linux).
- Run `./scripts/code.sh` (Mac/Linux).
- Run `./scripts/code.bat` (Windows).
6. Nice-to-knows.
- You can always press <kbd>Ctrl+R</kbd> (<kbd>Cmd+R</kbd>) inside the new window to reload and see your new changes. It's faster than <kbd>Ctrl+Shift+P</kbd> and `Reload Window`.
- You might want to add the flags `--user-data-dir ./.tmp/user-data --extensions-dir ./.tmp/extensions` to the above run command, which lets you delete the `.tmp` folder to reset any IDE changes you made when testing.
- 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
@@ -82,19 +70,28 @@ Alternatively, if you want to build Void from the terminal, instead of pressing
#### Common Fixes
### Common Fixes
- Make sure you followed the prerequisite steps.
- Make sure you have Node version `20.16.0` (the version in `.nvmrc`)!
- If you get `"TypeError: Failed to fetch dynamically imported module"`, make sure all imports end with `.js`.
- If you see missing styles, wait a few seconds and then reload.
- If you have any questions, feel free to [submit an issue](https://github.com/voideditor/void/issues/new). You can also refer to VSCode's complete [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute) page.
- Make sure you follow the prerequisite steps.
- Make sure you have the same NodeJS version as `.nvmrc`.
- Make sure your `npm run watchreact` is running if you change any React files, or else you'll need to re-build.
- If you see missing styles, go to `src2/styles.css` and re-save the file.
- If you get `"TypeError: Failed to fetch dynamically imported module: vscode-file://vscode-app/.../workbench.desktop.main.js", source: file:///.../bootstrap-window.js`, make sure all imports end with `.js`.
- If you have any questions, feel free to [submit an issue](https://github.com/voideditor/void/issues/new). For building questions, you can also refer to VSCode's full [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute) page.
## Packaging
## Bundling
We don't usually recommend packaging. Instead, you should probably just build. If you're sure you want to package Void into an executable app, make sure you've built first, then run one of the following commands. This will create a folder named `VSCode-darwin-arm64` or similar outside of the void/ repo (see below). Be patient - packaging can take ~25 minutes.
We don't usually recommend bundling. Instead, you should probably just build. If you're sure you want to bundle Void into an executable app, make sure you've built first, then run one of the following commands. This will create a folder named `VSCode-darwin-arm64` or similar outside of the void/ repo (see below). Be patient - compiling can take ~25 minutes.
```bash
workspace/
├── void/ # Your Void Fork
├── VSCode-linux-x64/ # Build folder generated outside of void for Linux
└── VSCode-darwin-arm64/ # Build folder generated outside of void for Mac
```
### Mac
@@ -111,27 +108,16 @@ We don't usually recommend packaging. Instead, you should probably just build. I
- `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
### Distributing
Void's maintainers distribute Void on our website and in releases. If you'd like to see the scripts to convert `Mac .app -> .dmg`, `Windows folder -> .exe`, and `Linux folder -> appimage` for distribution, feel free to reach out.
## Pull Request Guidelines
- Please submit a pull request once you've made a change.
- No need to submit an Issue unless you're creating a new feature that might involve multiple PRs.
- Please don't use AI to write your PR 🙂
We're always glad to talk about new ideas, help you get set up, and make sure your changes align with our vision for the project! Feel free to shoot Mat or Andrew a message, or start chatting with us in the `#contributing` channel of our [Discord](https://discord.gg/RSNjgaugJs).
## Submitting a Pull Request
- Please submit a pull request once you've made a change. No need to submit an Issue unless you're creating a new feature.
- Please don't use AI to write your PR 🙂.
<!--
@@ -139,8 +125,6 @@ Void's maintainers distribute Void on our website and in releases. If you'd like
We keep track of all the files we've changed with Void so it's easy to rebase:
Edit: far too many changes to track... this is old
- README.md
- CONTRIBUTING.md
- VOID_USEFUL_LINKS.md
@@ -160,5 +144,8 @@ Edit: far too many changes to track... this is old
- build/npm/dirs.js
- vscode.proposed.editorInsets.d.ts - not modified, but code copied
-->
## References
For some useful links we've compiled on VSCode, see [`VOID_USEFUL_LINKS.md`](https://github.com/voideditor/void/blob/main/VOID_USEFUL_LINKS.md).
+1 -1
View File
@@ -1,5 +1,5 @@
Void is a fork of VS Code, which is licensed under the MIT License (below).
Void's additions and modifications are licensed under the Apache 2.0 License (see LICENSE.txt).
Void's additions and modifications are licensed under the MIT License (see LICENSE.txt).
--------------------
+17 -197
View File
@@ -1,201 +1,21 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
MIT License
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
Copyright (c) 2025 Glass Devtools, Inc.
1. Definitions.
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:
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of 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.
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.
+6 -28
View File
@@ -1,39 +1,17 @@
# Welcome to Void.
<div align="center">
<img
src="./src/vs/workbench/browser/parts/editor/media/slice_of_void.png"
alt="Void Welcome"
width="300"
height="300"
/>
</div>
Void is the open-source Cursor alternative.
This repo contains the full sourcecode for Void. We are currently in [open beta](https://voideditor.com/email) for Discord members (see the `announcements` channel), with a waitlist for our official release. If you're new, welcome!
- 👋 [Discord](https://discord.gg/RSNjgaugJs)
- 🔨 [Contribute](https://github.com/voideditor/void/blob/main/CONTRIBUTING.md)
- 🚙 [Roadmap](https://github.com/orgs/voideditor/projects/2)
- 📝 [Changelog](https://voideditor.com/changelog)
Void is the open-source Cursor alternative. This repo contains the full sourcecode for Void. We have a [waitlist](https://voideditor.com/email) for downloading the official release, but you can build and develop Void right now.
If you're new, welcome!
## Contributing
1. Feel free to attend a weekly meeting in our Discord channel if you'd like to contribute!
2. To get started working on Void, see [Contributing](https://github.com/voideditor/void/blob/main/CONTRIBUTING.md).
3. We're open to collaborations and suggestions of all types - just reach out.
To build and run Void, follow the steps in [`CONTRIBUTING.md`](https://github.com/voideditor/void/blob/main/CONTRIBUTING.md).
## Reference
Void is a fork of the [vscode](https://github.com/microsoft/vscode) repository. For some useful links on VSCode, see [`VOID_USEFUL_LINKS.md`](https://github.com/voideditor/void/blob/main/VOID_USEFUL_LINKS.md).
Void is a fork of the 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 or contact us via email: hello@voideditor.com.
Feel free to reach out in our [Discord](https://discord.gg/RSNjgaugJs) server or contact us via email.
@@ -1,5 +1,3 @@
# Void - this looks like the relevant file for us (product-build-darwin.yml is independent and maybe just used for testing)
steps:
- task: NodeTool@0
inputs:
@@ -61,8 +59,6 @@ steps:
- script: node build/azure-pipelines/distro/mixin-quality
displayName: Mixin distro quality
## Void - IMPORTANT
- script: |
set -e
unzip $(Pipeline.Workspace)/unsigned_vscode_client_darwin_x64_archive/VSCode-darwin-x64.zip -d $(agent.builddirectory)/VSCode-darwin-x64
@@ -70,7 +66,6 @@ steps:
DEBUG=* node build/darwin/create-universal-app.js $(agent.builddirectory)
displayName: Create Universal App
## Void - IMPORTANT
- script: |
set -e
security create-keychain -p pwd $(agent.tempdirectory)/buildagent.keychain
-1
View File
@@ -4,7 +4,6 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
Object.defineProperty(exports, "__esModule", { value: true });
// Void explanation - product-build-darwin-universal.yml runs this (create-universal-app.ts), then sign.ts
const path = require("path");
const fs = require("fs");
const minimatch = require("minimatch");
-2
View File
@@ -3,8 +3,6 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// Void explanation - product-build-darwin-universal.yml runs this (create-universal-app.ts), then sign.ts
import * as path from 'path';
import * as fs from 'fs';
import * as minimatch from 'minimatch';
+4 -4
View File
@@ -78,24 +78,24 @@ async function main(buildDir) {
// universal will get its copy from the x64 build.
if (arch !== 'universal') {
await (0, cross_spawn_promise_1.spawn)('plutil', [
'-replace', // Void changed this to replace
'-insert',
'NSAppleEventsUsageDescription',
'-string',
'An application in Void wants to use AppleScript.',
'An application in Visual Studio Code wants to use AppleScript.',
`${infoPlistPath}`
]);
await (0, cross_spawn_promise_1.spawn)('plutil', [
'-replace',
'NSMicrophoneUsageDescription',
'-string',
'An application in Void wants to use the Microphone.',
'An application in Visual Studio Code wants to use the Microphone.',
`${infoPlistPath}`
]);
await (0, cross_spawn_promise_1.spawn)('plutil', [
'-replace',
'NSCameraUsageDescription',
'-string',
'An application in Void wants to use the Camera.',
'An application in Visual Studio Code wants to use the Camera.',
`${infoPlistPath}`
]);
}
+4 -4
View File
@@ -89,24 +89,24 @@ async function main(buildDir?: string): Promise<void> {
// universal will get its copy from the x64 build.
if (arch !== 'universal') {
await spawn('plutil', [
'-replace', // Void changed this to replace
'-insert',
'NSAppleEventsUsageDescription',
'-string',
'An application in Void wants to use AppleScript.',
'An application in Visual Studio Code wants to use AppleScript.',
`${infoPlistPath}`
]);
await spawn('plutil', [
'-replace',
'NSMicrophoneUsageDescription',
'-string',
'An application in Void wants to use the Microphone.',
'An application in Visual Studio Code wants to use the Microphone.',
`${infoPlistPath}`
]);
await spawn('plutil', [
'-replace',
'NSCameraUsageDescription',
'-string',
'An application in Void wants to use the Camera.',
'An application in Visual Studio Code wants to use the Camera.',
`${infoPlistPath}`
]);
}
-2
View File
@@ -341,8 +341,6 @@ function packageTask(platform, arch, sourceFolderName, destinationFolderName, op
this.emit('data', file);
}));
// Void - this is important, creates the product.json in .app
let productJsonContents;
const productJsonStream = gulp.src(['product.json'], { base: '.' })
.pipe(json({ commit, date: readISODate('out-build'), checksums, version }))
+1 -1
View File
@@ -23,7 +23,7 @@ AppMutex={code:GetAppMutex}
SetupMutex={#AppMutex}setup
; this is a Void icon comment. Old: WizardImageFile="{#RepoDir}\resources\win32\inno-big-100.bmp,{#RepoDir}\resources\win32\inno-big-125.bmp,{#RepoDir}\resources\win32\inno-big-150.bmp,{#RepoDir}\resources\win32\inno-big-175.bmp,{#RepoDir}\resources\win32\inno-big-200.bmp,{#RepoDir}\resources\win32\inno-big-225.bmp,{#RepoDir}\resources\win32\inno-big-250.bmp"
; this is a Void icon comment. Old: WizardSmallImageFile="{#RepoDir}\resources\win32\inno-small-100.bmp,{#RepoDir}\resources\win32\inno-small-125.bmp,{#RepoDir}\resources\win32\inno-small-150.bmp,{#RepoDir}\resources\win32\inno-small-175.bmp,{#RepoDir}\resources\win32\inno-small-200.bmp,{#RepoDir}\resources\win32\inno-small-225.bmp,{#RepoDir}\resources\win32\inno-small-250.bmp"
; WizardImageFile="{#RepoDir}\resources\win32\inno-void.bmp"
WizardImageFile="{#RepoDir}\resources\win32\inno-void.bmp"
WizardSmallImageFile="{#RepoDir}\resources\win32\inno-void.bmp"
SetupIconFile={#RepoDir}\resources\win32\code.ico
UninstallDisplayIcon={app}\{#ExeBasename}.exe
-112
View File
@@ -1,112 +0,0 @@
#!/bin/bash
set -e # Exit on error
set -x # Print commands as they are executed
# Configuration
APP_NAME="void"
APP_VERSION="1.0.0"
ARCH="x86_64"
export ARCH
# Check if void binary exists in current directory
if [ ! -f "./void" ]; then
echo "Error: void binary not found in current directory"
exit 1
fi
# Check if icon exists
if [ ! -f "./void.png" ]; then
echo "Error: void.png icon not found in current directory"
exit 1
fi
# Create temporary directory
TEMP_DIR="$(mktemp -d)"
echo "Created temporary directory: $TEMP_DIR"
APP_DIR="$TEMP_DIR/$APP_NAME.AppDir"
# Create basic AppDir structure
mkdir -pv "$APP_DIR/usr/bin"
mkdir -pv "$APP_DIR/usr/lib"
mkdir -pv "$APP_DIR/usr/share/applications"
mkdir -pv "$APP_DIR/usr/share/icons/hicolor/256x256/apps"
# Exclude create-appimage.sh and appimagetool-x86_64.AppImage from being copied
echo "Copying files excluding create-appimage.sh and appimagetool-x86_64.AppImage..."
for file in ./*; do
if [[ "$file" != "./create-appimage.sh" && "$file" != "./appimagetool-x86_64.AppImage" ]]; then
cp -rv "$file" "$APP_DIR/usr/bin/"
fi
done
# Copy the icon to required locations
cp -v ./void.png "$APP_DIR/void.png"
cp -v ./void.png "$APP_DIR/usr/share/icons/hicolor/256x256/apps/void.png"
# Copy dependencies with error checking
echo "Copying dependencies..."
for lib in $(ldd ./void | grep "=> /" | awk '{print $3}'); do
if [ -f "$lib" ]; then
cp -v "$lib" "$APP_DIR/usr/lib/" || echo "Failed to copy $lib"
else
echo "Warning: Library $lib not found"
fi
done
# Create desktop file with error checking
echo "Creating desktop file..."
if ! cat > "$APP_DIR/$APP_NAME.desktop" <<EOF
[Desktop Entry]
Name=$APP_NAME
Exec=void
Icon=void
Type=Application
Categories=Utility;
Comment=Void Linux Application
EOF
then
echo "Error creating desktop file"
exit 1
fi
# Make desktop file executable
chmod +x "$APP_DIR/$APP_NAME.desktop"
# Copy the desktop file to the applications directory
cp -v "$APP_DIR/$APP_NAME.desktop" "$APP_DIR/usr/share/applications/"
# Create AppRun with error checking
echo "Creating AppRun..."
if ! cat > "$APP_DIR/AppRun" <<EOF
#!/bin/bash
cd "\$(dirname "\$0")/usr/bin"
export LD_LIBRARY_PATH="\$APPDIR/usr/lib:\$LD_LIBRARY_PATH"
exec ./void "\$@"
EOF
then
echo "Error creating AppRun"
exit 1
fi
# Make AppRun executable
chmod +x "$APP_DIR/AppRun"
# Download appimagetool if not present in the current directory
if [ ! -f "./appimagetool-x86_64.AppImage" ]; then
echo "Downloading appimagetool-x86_64.AppImage..."
wget "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage"
chmod +x appimagetool-x86_64.AppImage
else
echo "appimagetool-x86_64.AppImage is already present."
fi
# Create the AppImage
echo "Creating AppImage..."
ARCH=x86_64 ./appimagetool-x86_64.AppImage "$APP_DIR" "${APP_NAME}-${APP_VERSION}-${ARCH}.AppImage"
# Cleanup
echo "Cleaning up..."
rm -rf "$TEMP_DIR"
echo "AppImage creation complete!"
@@ -0,0 +1,333 @@
import * as vscode from 'vscode';
import Parser from 'tree-sitter';
import JavaScript from 'tree-sitter-javascript';
interface Definition {
file: string;
node: Parser.SyntaxNode;
}
interface DefnUse {
parent: Parser.SyntaxNode;
file: string;
}
interface ImportInfo {
source: string;
imported: string;
}
class ProjectAnalyzer {
private parser: Parser;
private graph: Map<string, Set<string>>;
private visited: Set<string>;
private parsedFiles: Map<string, Parser.Tree>;
private imports: Map<string, Map<string, ImportInfo>>;
private definitions: Map<string, Definition>;
private fileStack: Set<string>;
constructor() {
this.parser = new Parser();
this.parser.setLanguage(JavaScript);
this.graph = new Map();
this.visited = new Set();
this.parsedFiles = new Map();
this.imports = new Map();
this.definitions = new Map();
this.fileStack = new Set();
}
async parseFile(filePath: string): Promise<Parser.Tree | null> {
if (this.parsedFiles.has(filePath)) {
return this.parsedFiles.get(filePath)!;
}
if (this.fileStack.has(filePath)) {
return null; // Circular import
}
this.fileStack.add(filePath);
try {
const uri = vscode.Uri.file(filePath);
const document = await vscode.workspace.openTextDocument(uri);
const code = document.getText();
const tree = this.parser.parse(code);
this.parsedFiles.set(filePath, tree);
this.collectImports(filePath, tree);
this.collectDefinitions(filePath, tree);
return tree;
} catch (error) {
console.error(`Error parsing ${filePath}:`, error);
return null;
} finally {
this.fileStack.delete(filePath);
}
}
private collectImports(filePath: string, tree: Parser.Tree): void {
const fileImports = new Map<string, ImportInfo>();
const visit = (node: Parser.SyntaxNode): void => {
if (node.type === 'import_declaration') {
const source = node.childForFieldName('source')?.text.slice(1, -1) ?? '';
const specifiers = node.childForFieldName('specifiers');
specifiers?.children.forEach(spec => {
if (spec.type === 'import_specifier') {
const local = spec.childForFieldName('local')?.text ?? '';
const imported = spec.childForFieldName('imported')?.text ?? '';
fileImports.set(local, { source, imported });
}
});
}
node.children.forEach(visit);
};
visit(tree.rootNode);
this.imports.set(filePath, fileImports);
}
private collectDefinitions(filePath: string, tree: Parser.Tree): void {
const visit = (node: Parser.SyntaxNode): void => {
if (node.type === 'function_declaration') {
const name = node.childForFieldName('name')?.text ?? '';
this.definitions.set(name, { file: filePath, node });
}
else if (node.type === 'variable_declarator') {
const name = node.childForFieldName('name')?.text;
const value = node.childForFieldName('value');
if (name && (value?.type === 'arrow_function' || value?.type === 'function')) {
this.definitions.set(name, { file: filePath, node: value });
}
}
node.children.forEach(visit);
};
visit(tree.rootNode);
}
private async getTypeFromPosition(uri: vscode.Uri, position: vscode.Position): Promise<string | null> {
const hover = await vscode.commands.executeCommand<vscode.Hover[]>(
'vscode.executeHoverProvider',
uri,
position
);
if (hover?.[0]?.contents.length) {
for (const content of hover[0].contents) {
let hoverText = typeof content === 'string' ?
content :
('value' in content ? content.value : '');
// Remove typescript backticks if present
hoverText = hoverText.replace(/```typescript\s*/, '').replace(/```\s*$/, '');
console.log('Processing hover text:', hoverText);
// Extract the type information - look for the type after the colon
const typeMatches = [
/:\s*([\w<>]+)(?:\[\])?/, // matches "foo: Type" or "foo: Type[]"
/var\s+\w+:\s*([\w<>]+)/, // matches "var foo: Type"
/\(type\)\s+[\w<>]+:\s*([\w<>]+)/, // matches "(type) foo: Type"
/\(method\)\s*([\w<>]+)\./ // matches "(method) Type.method"
];
for (const pattern of typeMatches) {
const match = pattern.exec(hoverText);
if (match) {
let type = match[1];
// Handle array types
if (hoverText.includes('[]')) {
return 'Array';
}
// Extract base type from generics
if (type.includes('<')) {
type = type.split('<')[0];
}
return type;
}
}
}
}
return null;
}
private async getCallsInDefn(defnNode: Parser.SyntaxNode, currentFile: string): Promise<Set<string>> {
const calls = new Set<string>();
const fileImports = this.imports.get(currentFile) ?? new Map();
const uri = vscode.Uri.file(currentFile);
const visit = async (node: Parser.SyntaxNode): Promise<void> => {
if (node.type === 'call_expression') {
const callee = node.childForFieldName('function');
if (callee?.type === 'identifier') {
const name = callee.text;
const importInfo = fileImports.get(name);
if (importInfo) {
calls.add(`${importInfo.source}:${importInfo.imported}`);
} else {
calls.add(name);
}
}
else if (callee?.type === 'member_expression') {
const method = callee.childForFieldName('property')?.text;
const object = callee.childForFieldName('object');
if (method && object) {
const position = new vscode.Position(
object.startPosition.row,
object.startPosition.column
);
const type = await this.getTypeFromPosition(uri, position);
if (type) {
calls.add(`${type}.${method}`);
} else {
calls.add(`method:${method}`);
}
}
}
}
for (const child of node.children) {
await visit(child);
}
};
await visit(defnNode);
return calls;
}
private gotoDefn(name: string): Definition | null {
if (name.includes(':')) {
const [file, funcName] = name.split(':');
const def = this.definitions.get(funcName);
return def ?? null;
}
return this.definitions.get(name) ?? null;
}
private getUses(defnNode: Parser.SyntaxNode, currentFile: string): DefnUse[] {
const uses: DefnUse[] = [];
let fnName: string | undefined;
if (defnNode.type === 'function_declaration') {
fnName = defnNode.childForFieldName('name')?.text;
} else if (defnNode.type === 'arrow_function' || defnNode.type === 'function') {
const parent = defnNode.parent;
if (parent?.type === 'variable_declarator') {
fnName = parent.childForFieldName('name')?.text;
}
}
if (!fnName) return uses;
for (const [file, tree] of this.parsedFiles) {
const visit = (node: Parser.SyntaxNode): void => {
if (node.type === 'call_expression') {
const callee = node.childForFieldName('function');
if (callee?.type === 'identifier' && callee.text === fnName) {
let current: Parser.SyntaxNode | null = node;
while (current) {
if (current.type === 'function_declaration' ||
current.type === 'arrow_function' ||
current.type === 'function') {
uses.push({ parent: current, file });
break;
}
current = current.parent;
}
}
}
node.children.forEach(visit);
};
visit(tree.rootNode);
}
return uses;
}
private async visitAllNodesInGraphFromDefinition(defn: Parser.SyntaxNode, currentFile: string): Promise<void> {
let defnName: string | undefined;
if (defn.type === 'function_declaration') {
defnName = defn.childForFieldName('name')?.text;
} else if (defn.type === 'arrow_function' || defn.type === 'function') {
const parent = defn.parent;
if (parent?.type === 'variable_declarator') {
defnName = parent.childForFieldName('name')?.text;
}
}
if (!defnName) return;
const fullName = `${currentFile}:${defnName}`;
if (this.visited.has(fullName)) return;
const calls = await this.getCallsInDefn(defn, currentFile);
this.graph.set(fullName, calls);
this.visited.add(fullName);
const callDefns = Array.from(calls).map(call => this.gotoDefn(call));
for (const callDefn of callDefns) {
if (callDefn) {
await this.visitAllNodesInGraphFromDefinition(callDefn.node, callDefn.file);
}
}
const defnUses = this.getUses(defn, currentFile);
for (const defnUse of defnUses) {
await this.visitAllNodesInGraphFromDefinition(defnUse.parent, defnUse.file);
}
}
async analyze(entryFile: string): Promise<Map<string, Set<string>>> {
const tree = await this.parseFile(entryFile);
if (!tree) return new Map();
const visit = async (node: Parser.SyntaxNode): Promise<void> => {
if (node.type === 'function_declaration') {
await this.visitAllNodesInGraphFromDefinition(node, entryFile);
}
else if (node.type === 'variable_declarator') {
const value = node.childForFieldName('value');
if (value?.type === 'arrow_function' || value?.type === 'function') {
await this.visitAllNodesInGraphFromDefinition(value, entryFile);
}
}
for (const child of node.children) {
await visit(child);
}
};
await visit(tree.rootNode);
return this.graph;
}
}
export async function runTreeSitter(filePath?: string): Promise<Map<string, Set<string>> | null> {
const editor = vscode.window.activeTextEditor;
if (!editor && !filePath) {
vscode.window.showWarningMessage('No active editor found');
return null;
}
try {
const targetPath = filePath ?? editor!.document.uri.fsPath;
const analyzer = new ProjectAnalyzer();
const graph = await analyzer.analyze(targetPath);
for (const [defn, calls] of graph) {
console.log(`${defn} calls: ${[...calls].join(', ')}`);
}
return graph;
} catch (error) {
console.error('Error analyzing file:', error);
vscode.window.showErrorMessage('Error analyzing file');
return null;
}
}
@@ -0,0 +1,62 @@
import * as vscode from 'vscode';
const legend = new vscode.SemanticTokensLegend([], []);
export async function findFunctions() {
const editor = vscode.window.activeTextEditor;
if (!editor) return;
const document = editor.document;
const tokens = await vscode.commands.executeCommand<vscode.SemanticTokens>(
'vscode.provideDocumentSemanticTokens',
document.uri
);
if (!tokens) {
console.error('No tokens found');
return [];
}
const allTokens = decodeTokens(tokens, document);
return allTokens;
}
function decodeTokens(tokens: vscode.SemanticTokens, document: vscode.TextDocument) {
const data = tokens.data;
const decodedTokens = [];
let line = 0;
let character = 0;
for (let i = 0; i < data.length; i += 5) {
const deltaLine = data[i];
const deltaStartChar = data[i + 1];
const length = data[i + 2];
const tokenTypeIdx = data[i + 3];
const tokenModifierIdx = data[i + 4];
line += deltaLine;
character = deltaLine === 0 ? character + deltaStartChar : deltaStartChar;
const type = legend.tokenTypes[tokenTypeIdx] || `(${tokenTypeIdx})`;
const modifier = legend.tokenModifiers[tokenModifierIdx] || `(${tokenModifierIdx})`;
const tokenRange = new vscode.Range(line, character, line, character + length);
const tokenText = document.getText(tokenRange);
decodedTokens.push({
line,
startCharacter: character,
length,
type,
modifier,
text: tokenText,
});
console.log(`Token: '${tokenText}' | Type: ${type} | Modifier: ${modifier} | Line: ${line}, Character: ${character}`);
}
return decodedTokens;
}
+299 -898
View File
File diff suppressed because it is too large Load Diff
+3 -7
View File
@@ -14,7 +14,6 @@
"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 ../../../../../../../",
"watchreactd": "deemon npm run watchreact",
"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",
@@ -26,7 +25,7 @@
"preinstall": "node build/npm/preinstall.js",
"postinstall": "node build/npm/postinstall.js",
"compile": "node ./node_modules/gulp/bin/gulp.js compile",
"watch": "npm-run-all -lp watchreact watch-client watch-extensions",
"watch": "npm-run-all -lp watch-client watch-extensions",
"watch-amd": "npm-run-all -lp watch-client-amd watch-extensions",
"watchd": "deemon npm run watch",
"watch-webd": "deemon npm run watch-web",
@@ -79,14 +78,13 @@
},
"dependencies": {
"@anthropic-ai/sdk": "^0.32.1",
"@floating-ui/react": "^0.27.3",
"@google/generative-ai": "^0.21.0",
"@microsoft/1ds-core-js": "^3.2.13",
"@microsoft/1ds-post-js": "^3.2.13",
"@mistralai/mistralai": "^1.4.0",
"@parcel/watcher": "2.1.0",
"@rrweb/record": "^2.0.0-alpha.17",
"@rrweb/types": "^2.0.0-alpha.17",
"@todesktop/runtime": "^2.0.0",
"@vscode/deviceid": "^0.1.1",
"@vscode/iconv-lite-umd": "0.7.0",
"@vscode/policy-watcher": "^1.1.4",
@@ -135,8 +133,7 @@
"vscode-regexpp": "^3.1.0",
"vscode-textmate": "9.1.0",
"yauzl": "^3.0.0",
"yazl": "^2.4.3",
"zod": "^3.24.1"
"yazl": "^2.4.3"
},
"devDependencies": {
"@playwright/test": "^1.46.1",
@@ -227,7 +224,6 @@
"mocha": "^10.2.0",
"mocha-junit-reporter": "^2.2.1",
"mocha-multi-reporters": "^1.5.1",
"next": "^15.1.4",
"nodemon": "^3.1.9",
"npm-run-all": "^4.1.5",
"opn": "^6.0.0",
-7
View File
@@ -29,7 +29,6 @@
"@xterm/headless": "^5.6.0-beta.64",
"@xterm/xterm": "^5.6.0-beta.64",
"cookie": "^0.4.0",
"debounced": "1.0.2",
"http-proxy-agent": "^7.0.0",
"https-proxy-agent": "^7.0.2",
"jschardet": "3.1.3",
@@ -397,12 +396,6 @@
"node": ">= 0.6"
}
},
"node_modules/debounced": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/debounced/-/debounced-1.0.2.tgz",
"integrity": "sha512-6GPv+l/OOtdb1DKNY70k5ubuJhVjtBjUnujC5vQAHHrMuvBpDXsTc91xEMTdeA3/v4swYHamtdB9XIN7DcKxpw==",
"license": "MIT"
},
"node_modules/debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
-1
View File
@@ -26,7 +26,6 @@
"cookie": "^0.4.0",
"http-proxy-agent": "^7.0.0",
"https-proxy-agent": "^7.0.2",
"debounced": "1.0.2",
"jschardet": "3.1.3",
"kerberos": "2.1.1",
"minimist": "^1.2.6",
+3 -3
View File
@@ -1,9 +1,9 @@
<Application xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<VisualElements
BackgroundColor="#FFFFFF"
BackgroundColor="#2D2D30"
ShowNameOnSquare150x150Logo="on"
Square150x150Logo="resources\app\resources\win32\code_150x150.png"
Square70x70Logo="resources\app\resources\win32\code_70x70.png"
ForegroundText="light"
ShortDisplayName="Void" />
ForegroundText="light"
ShortDisplayName="Code - OSS" />
</Application>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 173 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 5.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 795 KiB

+4 -10
View File
@@ -121,11 +121,10 @@ import { normalizeNFC } from '../../base/common/normalization.js';
import { ICSSDevelopmentService, CSSDevelopmentService } from '../../platform/cssDev/node/cssDevService.js';
import { ExtensionSignatureVerificationService, IExtensionSignatureVerificationService } from '../../platform/extensionManagement/node/extensionSignatureVerificationService.js';
import { LLMMessageChannel } from '../../workbench/contrib/void/electron-main/llmMessageChannel.js';
import { IMetricsService } from '../../workbench/contrib/void/common/metricsService.js';
import { MetricsMainService } from '../../workbench/contrib/void/electron-main/metricsMainService.js';
import { VoidMainUpdateService } from '../../workbench/contrib/void/electron-main/voidUpdateMainService.js';
import { IVoidUpdateService } from '../../workbench/contrib/void/common/voidUpdateService.js';
import { LLMMessageChannel } from '../../platform/void/electron-main/llmMessageChannel.js';
import { IMetricsService } from '../../platform/void/common/metricsService.js';
import { MetricsMainService } from '../../platform/void/electron-main/metricsMainService.js';
/**
* The main VS Code application. There will only ever be one instance,
* even if the user starts many instances (e.g. from the command line).
@@ -1108,7 +1107,6 @@ export class CodeApplication extends Disposable {
// Void main process services (required for services with a channel for comm between browser and electron-main (node))
services.set(IMetricsService, new SyncDescriptor(MetricsMainService, undefined, false));
services.set(IVoidUpdateService, new SyncDescriptor(VoidMainUpdateService, undefined, false));
// Default Extensions Profile Init
services.set(IExtensionsProfileScannerService, new SyncDescriptor(ExtensionsProfileScannerService, undefined, true));
@@ -1247,10 +1245,6 @@ export class CodeApplication extends Disposable {
// Void - use loggerChannel as reference
const metricsChannel = ProxyChannel.fromService(accessor.get(IMetricsService), disposables);
mainProcessElectronServer.registerChannel('void-channel-metrics', metricsChannel);
const voidUpdatesChannel = ProxyChannel.fromService(accessor.get(IVoidUpdateService), disposables);
mainProcessElectronServer.registerChannel('void-channel-update', voidUpdatesChannel);
const llmMessageChannel = new LLMMessageChannel(accessor.get(IMetricsService));
mainProcessElectronServer.registerChannel('void-channel-llmMessageService', llmMessageChannel);
+3
View File
@@ -6,6 +6,7 @@
import '../../platform/update/common/update.config.contribution.js';
import { app, dialog } from 'electron';
import todesktop from '@todesktop/runtime'
import { unlinkSync, promises } from 'fs';
import { URI } from '../../base/common/uri.js';
import { coalesce, distinct } from '../../base/common/arrays.js';
@@ -94,6 +95,8 @@ class CodeMain {
private async startup(): Promise<void> {
todesktop.init();
// Set the error handler early enough so that we are not getting the
// default electron error dialog popping up
setUnexpectedErrorHandler(err => console.error(err));
@@ -223,11 +223,6 @@ export class BaseEditorSimpleWorker implements IDisposable, IWorkerTextModelSync
private static readonly _diffLimit = 100000;
public async $computeMoreMinimalEdits(modelUrl: string, edits: TextEdit[], pretty: boolean): Promise<TextEdit[]> {
return this.$Void_computeMoreMinimalEdits(modelUrl, edits, pretty)
}
// Void added this as non async
public $Void_computeMoreMinimalEdits(modelUrl: string, edits: TextEdit[], pretty: boolean): TextEdit[] {
const model = this._getModel(modelUrl);
if (!model) {
return edits;
@@ -403,7 +403,6 @@ function isVersionValid(currentVersion: string, date: ProductDate, requestedVers
}
if (!isValidVersion(currentVersion, date, desiredVersion)) {
// Void - ignore not compatible
notices.push(nls.localize('versionMismatch', "Extension is not compatible with Code {0}. Extension requires: {1}.", currentVersion, requestedVersion));
return false;
}
@@ -64,8 +64,7 @@ export const enum KeybindingWeight {
EditorContrib = 100,
WorkbenchContrib = 200,
BuiltinExtension = 300,
ExternalExtension = 400,
VoidExtension = 605, // Void - must trump any external extension
ExternalExtension = 400
}
export interface ICommandAndKeybindingRule extends IKeybindingRule {
@@ -31,29 +31,25 @@ export class ServerTelemetryService extends TelemetryService implements IServerT
}
override publicLog(eventName: string, data?: ITelemetryData) {
// Void commented this out
// if (this._injectedTelemetryLevel < TelemetryLevel.USAGE) {
// return;
// }
// return super.publicLog(eventName, data);
if (this._injectedTelemetryLevel < TelemetryLevel.USAGE) {
return;
}
return super.publicLog(eventName, data);
}
override publicLog2<E extends ClassifiedEvent<OmitMetadata<T>> = never, T extends IGDPRProperty = never>(eventName: string, data?: StrictPropertyCheck<T, E>) {
// Void commented this out
// return this.publicLog(eventName, data as ITelemetryData | undefined);
return this.publicLog(eventName, data as ITelemetryData | undefined);
}
override publicLogError(errorEventName: string, data?: ITelemetryData) {
// Void commented this out
// if (this._injectedTelemetryLevel < TelemetryLevel.ERROR) {
// return Promise.resolve(undefined);
// }
// return super.publicLogError(errorEventName, data);
if (this._injectedTelemetryLevel < TelemetryLevel.ERROR) {
return Promise.resolve(undefined);
}
return super.publicLogError(errorEventName, data);
}
override publicLogError2<E extends ClassifiedEvent<OmitMetadata<T>> = never, T extends IGDPRProperty = never>(eventName: string, data?: StrictPropertyCheck<T, E>) {
// Void commented this out
// return this.publicLogError(eventName, data as ITelemetryData | undefined);
return this.publicLogError(eventName, data as ITelemetryData | undefined);
}
async updateInjectedTelemetryLevel(telemetryLevel: TelemetryLevel): Promise<void> {
@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { DisposableStore } from '../../../base/common/lifecycle.js';
// import { mixin } from '../../../base/common/objects.js';
import { mixin } from '../../../base/common/objects.js';
import { isWeb } from '../../../base/common/platform.js';
import { escapeRegExpCharacters } from '../../../base/common/strings.js';
import { localize } from '../../../nls.js';
@@ -15,8 +15,7 @@ import { IProductService } from '../../product/common/productService.js';
import { Registry } from '../../registry/common/platform.js';
import { ClassifiedEvent, IGDPRProperty, OmitMetadata, StrictPropertyCheck } from './gdprTypings.js';
import { ITelemetryData, ITelemetryService, TelemetryConfiguration, TelemetryLevel, TELEMETRY_CRASH_REPORTER_SETTING_ID, TELEMETRY_OLD_SETTING_ID, TELEMETRY_SECTION_ID, TELEMETRY_SETTING_ID, ICommonProperties } from './telemetry.js';
import { getTelemetryLevel, ITelemetryAppender } from './telemetryUtils.js';
// import { cleanData } from './telemetryUtils.js';
import { cleanData, getTelemetryLevel, ITelemetryAppender } from './telemetryUtils.js';
export interface ITelemetryServiceConfig {
appenders: ITelemetryAppender[];
@@ -39,7 +38,7 @@ export class TelemetryService implements ITelemetryService {
readonly firstSessionDate: string;
readonly msftInternal: boolean | undefined;
// private _appenders: ITelemetryAppender[];
private _appenders: ITelemetryAppender[];
private _commonProperties: ICommonProperties;
private _experimentProperties: { [name: string]: string } = {};
private _piiPaths: string[];
@@ -54,7 +53,7 @@ export class TelemetryService implements ITelemetryService {
@IConfigurationService private _configurationService: IConfigurationService,
@IProductService private _productService: IProductService
) {
// this._appenders = config.appenders;
this._appenders = config.appenders;
this._commonProperties = config.commonProperties ?? Object.create(null);
this.sessionId = this._commonProperties['sessionID'] as string;
@@ -122,47 +121,44 @@ export class TelemetryService implements ITelemetryService {
this._disposables.dispose();
}
// Void commented this out
// private _log(eventName: string, eventLevel: TelemetryLevel, data?: ITelemetryData) {
// // don't send events when the user is optout
// if (this._telemetryLevel < eventLevel) {
// return;
// }
private _log(eventName: string, eventLevel: TelemetryLevel, data?: ITelemetryData) {
// don't send events when the user is optout
if (this._telemetryLevel < eventLevel) {
return;
}
// // add experiment properties
// data = mixin(data, this._experimentProperties);
// add experiment properties
data = mixin(data, this._experimentProperties);
// // remove all PII from data
// data = cleanData(data as Record<string, any>, this._cleanupPatterns);
// remove all PII from data
data = cleanData(data as Record<string, any>, this._cleanupPatterns);
// // add common properties
// data = mixin(data, this._commonProperties);
// add common properties
data = mixin(data, this._commonProperties);
// // Log to the appenders of sufficient level
// this._appenders.forEach(a => a.log(eventName, data));
// }
// Log to the appenders of sufficient level
this._appenders.forEach(a => a.log(eventName, data));
}
publicLog(eventName: string, data?: ITelemetryData) {
// this._log(eventName, TelemetryLevel.USAGE, data);
this._log(eventName, TelemetryLevel.USAGE, data);
}
publicLog2<E extends ClassifiedEvent<OmitMetadata<T>> = never, T extends IGDPRProperty = never>(eventName: string, data?: StrictPropertyCheck<T, E>) {
// this.publicLog(eventName, data as ITelemetryData);
this.publicLog(eventName, data as ITelemetryData);
}
publicLogError(errorEventName: string, data?: ITelemetryData) {
// Void commented this out
// if (!this._sendErrorTelemetry) {
// return;
// }
if (!this._sendErrorTelemetry) {
return;
}
// // Send error event and anonymize paths
// this._log(errorEventName, TelemetryLevel.ERROR, data);
// Send error event and anonymize paths
this._log(errorEventName, TelemetryLevel.ERROR, data);
}
publicLogError2<E extends ClassifiedEvent<OmitMetadata<T>> = never, T extends IGDPRProperty = never>(eventName: string, data?: StrictPropertyCheck<T, E>) {
// Void commented this out
// this.publicLogError(eventName, data as ITelemetryData);
this.publicLogError(eventName, data as ITelemetryData);
}
}
@@ -3,12 +3,11 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// import { timeout } from '../../../base/common/async.js';
import { timeout } from '../../../base/common/async.js';
import { CancellationToken } from '../../../base/common/cancellation.js';
import { Emitter, Event } from '../../../base/common/event.js';
import { IConfigurationService } from '../../configuration/common/configuration.js';
import { IEnvironmentMainService } from '../../environment/electron-main/environmentMainService.js';
// import { IEnvironmentMainService } from '../../environment/electron-main/environmentMainService.js';
import { ILifecycleMainService, LifecycleMainPhase } from '../../lifecycle/electron-main/lifecycleMainService.js';
import { ILogService } from '../../log/common/log.js';
import { IProductService } from '../../product/common/productService.js';
@@ -16,10 +15,7 @@ import { IRequestService } from '../../request/common/request.js';
import { AvailableForDownload, DisablementReason, IUpdateService, State, StateType, UpdateType } from '../common/update.js';
export function createUpdateURL(platform: string, quality: string, productService: IProductService): string {
// return `https://voideditor.dev/api/update/${platform}/stable`;
// return `${productService.updateUrl}/api/update/${platform}/${quality}/${productService.commit}`;
// https://github.com/VSCodium/update-api
return `https://updates.voideditor.dev/api/update/${platform}/${quality}/${productService.commit}`;
return `${productService.updateUrl}/api/update/${platform}/${quality}/${productService.commit}`;
}
export type UpdateNotAvailableClassification = {
@@ -61,7 +57,7 @@ export abstract class AbstractUpdateService implements IUpdateService {
@IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService,
@IRequestService protected requestService: IRequestService,
@ILogService protected logService: ILogService,
@IProductService protected readonly productService: IProductService,
@IProductService protected readonly productService: IProductService
) {
lifecycleMainService.when(LifecycleMainPhase.AfterWindowOpen)
.finally(() => this.initialize());
@@ -74,35 +70,75 @@ export abstract class AbstractUpdateService implements IUpdateService {
*/
protected async initialize(): Promise<void> {
if (!this.environmentMainService.isBuilt) {
console.log('is NOT built, canceling update service')
this.setState(State.Disabled(DisablementReason.NotBuilt));
return; // updates are never enabled when running out of sources
}
console.log('is built, continuing with update service')
this.url = this.doBuildUpdateFeedUrl('stable');
if (this.environmentMainService.disableUpdates) {
this.setState(State.Disabled(DisablementReason.DisabledByEnvironment));
this.logService.info('update#ctor - updates are disabled by the environment');
return;
}
if (!this.productService.updateUrl || !this.productService.commit) {
this.setState(State.Disabled(DisablementReason.MissingConfiguration));
this.logService.info('update#ctor - updates are disabled as there is no update URL');
return;
}
const updateMode = this.configurationService.getValue<'none' | 'manual' | 'start' | 'default'>('update.mode');
const quality = this.getProductQuality(updateMode);
if (!quality) {
this.setState(State.Disabled(DisablementReason.ManuallyDisabled));
this.logService.info('update#ctor - updates are disabled by user preference');
return;
}
this.url = this.buildUpdateFeedUrl(quality);
if (!this.url) {
this.setState(State.Disabled(DisablementReason.InvalidConfiguration));
this.logService.info('update#ctor - updates are disabled as the update URL is badly formed');
return;
}
this.setState(State.Disabled(DisablementReason.ManuallyDisabled));
// hidden setting
if (this.configurationService.getValue<boolean>('_update.prss')) {
const url = new URL(this.url);
url.searchParams.set('prss', 'true');
this.url = url.toString();
}
this.setState(State.Idle(this.getUpdateType()));
// Void - temporarily disabled while we figure out how to do this the right way
if (updateMode === 'manual') {
this.logService.info('update#ctor - manual checks only; automatic updates are disabled by user preference');
return;
}
// this.setState(State.Idle(this.getUpdateType()));
if (updateMode === 'start') {
this.logService.info('update#ctor - startup checks only; automatic updates are disabled by user preference');
// start checking for updates after 10 seconds
// this.scheduleCheckForUpdates(10 * 1000).then(undefined, err => this.logService.error(err));
// Check for updates only once after 30 seconds
setTimeout(() => this.checkForUpdates(false), 30 * 1000);
} else {
// Start checking for updates after 30 seconds
this.scheduleCheckForUpdates(30 * 1000).then(undefined, err => this.logService.error(err));
}
}
// private async scheduleCheckForUpdates(delay = 60 * 60 * 1000): Promise<void> {
// await timeout(delay);
// await this.checkForUpdates(false);
// return await this.scheduleCheckForUpdates(60 * 60 * 1000);
// }
private getProductQuality(updateMode: string): string | undefined {
return updateMode === 'none' ? undefined : this.productService.quality;
}
private scheduleCheckForUpdates(delay = 60 * 60 * 1000): Promise<void> {
return timeout(delay)
.then(() => this.checkForUpdates(false))
.then(() => {
// Check again after 1 hour
return this.scheduleCheckForUpdates(60 * 60 * 1000);
});
}
async checkForUpdates(explicit: boolean): Promise<void> {
this.logService.trace('update#checkForUpdates, state = ', this.state.type);
@@ -124,7 +160,6 @@ export abstract class AbstractUpdateService implements IUpdateService {
await this.doDownloadUpdate(this.state);
}
// override implemented by windows and linux
protected async doDownloadUpdate(state: AvailableForDownload): Promise<void> {
// noop
}
@@ -139,7 +174,6 @@ export abstract class AbstractUpdateService implements IUpdateService {
await this.doApplyUpdate();
}
// windows overrides this
protected async doApplyUpdate(): Promise<void> {
// noop
}
@@ -202,6 +236,6 @@ export abstract class AbstractUpdateService implements IUpdateService {
// noop
}
protected abstract doBuildUpdateFeedUrl(quality: string): string | undefined;
protected abstract buildUpdateFeedUrl(quality: string): string | undefined;
protected abstract doCheckForUpdates(context: any): void;
}
@@ -34,7 +34,7 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau
@IEnvironmentMainService environmentMainService: IEnvironmentMainService,
@IRequestService requestService: IRequestService,
@ILogService logService: ILogService,
@IProductService productService: IProductService,
@IProductService productService: IProductService
) {
super(lifecycleMainService, configurationService, environmentMainService, requestService, logService, productService);
@@ -73,7 +73,7 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau
this.setState(State.Idle(UpdateType.Archive, message));
}
protected doBuildUpdateFeedUrl(quality: string): string | undefined {
protected buildUpdateFeedUrl(quality: string): string | undefined {
let assetID: string;
if (!this.productService.darwinUniversalAssetId) {
assetID = process.arch === 'x64' ? 'darwin' : 'darwin-arm64';
@@ -30,7 +30,7 @@ export class LinuxUpdateService extends AbstractUpdateService {
super(lifecycleMainService, configurationService, environmentMainService, requestService, logService, productService);
}
protected doBuildUpdateFeedUrl(quality: string): string {
protected buildUpdateFeedUrl(quality: string): string {
return createUpdateURL(`linux-${process.arch}`, quality, this.productService);
}
@@ -67,7 +67,7 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun
@ILogService logService: ILogService,
@IFileService private readonly fileService: IFileService,
@INativeHostMainService private readonly nativeHostMainService: INativeHostMainService,
@IProductService productService: IProductService,
@IProductService productService: IProductService
) {
super(lifecycleMainService, configurationService, environmentMainService, requestService, logService, productService);
@@ -99,7 +99,7 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun
await super.initialize();
}
protected doBuildUpdateFeedUrl(quality: string): string | undefined {
protected buildUpdateFeedUrl(quality: string): string | undefined {
let platform = `win32-${process.arch}`;
if (getUpdateType() === UpdateType.Archive) {
@@ -0,0 +1,18 @@
/*------------------------------------------------------------------------------------------
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
*-----------------------------------------------------------------------------------------*/
// ---------- common ----------
// llmMessage
import '../common/llmMessageService.js'
// voidSettings
import '../common/voidSettingsService.js'
// refreshModel
import '../common/refreshModelService.js'
// metrics
import '../common/metricsService.js'
@@ -1,19 +1,17 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
*-----------------------------------------------------------------------------------------*/
import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, ServiceSendLLMMessageParams, MainSendLLMMessageParams, MainLLMMessageAbortParams, ServiceModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, MainModelListParams, OllamaModelResponse, OpenaiCompatibleModelResponse, } from './llmMessageTypes.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
import { IChannel } from '../../../../base/parts/ipc/common/ipc.js';
import { IMainProcessService } from '../../../../platform/ipc/common/mainProcessService.js';
import { generateUuid } from '../../../../base/common/uuid.js';
import { Event } from '../../../../base/common/event.js';
import { Disposable } from '../../../../base/common/lifecycle.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';
import { generateUuid } from '../../../base/common/uuid.js';
import { createDecorator } from '../../instantiation/common/instantiation.js';
import { Event } from '../../../base/common/event.js';
import { Disposable } from '../../../base/common/lifecycle.js';
import { IVoidSettingsService } from './voidSettingsService.js';
import { displayInfoOfProviderName, isFeatureNameDisabled } from './voidSettingsTypes.js';
// import { INotificationService } from '../../notification/common/notification.js';
// calls channel to implement features
@@ -54,7 +52,6 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
super()
// const service = ProxyChannel.toService<LLMMessageChannel>(mainProcessService.getChannel('void-channel-sendLLMMessage')); // lets you call it like a service
// see llmMessageChannel.ts
this.channel = this.mainProcessService.getChannel('void-channel-llmMessageService')
// .listen sets up an IPC channel and takes a few ms, so we set up listeners immediately and add hooks to them instead
@@ -67,18 +64,18 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
this._onRequestIdDone(e.requestId)
}))
this._register((this.channel.listen('onError_llm') satisfies Event<EventLLMMessageOnErrorParams>)(e => {
console.error('Error in LLMMessageService:', JSON.stringify(e))
console.log('Error in LLMMessageService:', JSON.stringify(e))
this.onErrorHooks_llm[e.requestId]?.(e)
this._onRequestIdDone(e.requestId)
}))
// ollama .list()
// ollama
this._register((this.channel.listen('onSuccess_ollama') satisfies Event<EventModelListOnSuccessParams<OllamaModelResponse>>)(e => {
this.onSuccess_ollama[e.requestId]?.(e)
}))
this._register((this.channel.listen('onError_ollama') satisfies Event<EventModelListOnErrorParams<OllamaModelResponse>>)(e => {
this.onError_ollama[e.requestId]?.(e)
}))
// openaiCompatible .list()
// openaiCompatible
this._register((this.channel.listen('onSuccess_openAICompatible') satisfies Event<EventModelListOnSuccessParams<OpenaiCompatibleModelResponse>>)(e => {
this.onSuccess_openAICompatible[e.requestId]?.(e)
}))
@@ -90,51 +87,34 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
sendLLMMessage(params: ServiceSendLLMMessageParams) {
const { onText, onFinalMessage, onError, ...proxyParams } = params;
const { useProviderFor: featureName } = proxyParams
const { featureName } = proxyParams
// throw an error if no model/provider selected (this should usually never be reached, the UI should check this first, but might happen in cases like Apply where we haven't built much UI/checks yet, good practice to have check logic on backend)
const isDisabled = isFeatureNameDisabled(featureName, this.voidSettingsService.state)
// end early if no provider
const modelSelection = this.voidSettingsService.state.modelSelectionOfFeature[featureName]
if (isDisabled || modelSelection === null) {
let message: string
if (isDisabled === 'addProvider' || isDisabled === 'providerNotAutoDetected')
message = `Please add a provider in Void Settings.`
else if (isDisabled === 'addModel')
message = `Please add a model.`
else if (isDisabled === 'needToEnableModel')
message = `Please enable a model.`
else if (isDisabled === 'notFilledIn')
message = `Please fill in Void Settings${modelSelection !== null ? ` for ${displayInfoOfProviderName(modelSelection.providerName).title}` : ''}.`
else
message = 'Please add a provider in Void Settings.'
onError({ message, fullError: null })
if (modelSelection === null) {
onError({ message: 'Please add a Provider in Settings!', fullError: null })
return null
}
const { providerName, modelName } = modelSelection
// add state for request id
const requestId = generateUuid();
this.onTextHooks_llm[requestId] = onText
this.onFinalMessageHooks_llm[requestId] = onFinalMessage
this.onErrorHooks_llm[requestId] = onError
const requestId_ = generateUuid();
this.onTextHooks_llm[requestId_] = onText
this.onFinalMessageHooks_llm[requestId_] = onFinalMessage
this.onErrorHooks_llm[requestId_] = onError
const { aiInstructions } = this.voidSettingsService.state.globalSettings
const { settingsOfProvider } = this.voidSettingsService.state
// params will be stripped of all its functions over the IPC channel
this.channel.call('sendLLMMessage', {
...proxyParams,
aiInstructions,
requestId,
requestId: requestId_,
providerName,
modelName,
settingsOfProvider,
} satisfies MainSendLLMMessageParams);
} satisfies MainLLMMessageParams);
return requestId
return requestId_
}
@@ -161,7 +141,6 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
} satisfies MainModelListParams<OllamaModelResponse>)
}
openAICompatibleList = (params: ServiceModelListParams<OpenaiCompatibleModelResponse>) => {
const { onSuccess, onError, ...proxyParams } = params
@@ -1,118 +1,82 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
*-----------------------------------------------------------------------------------------*/
import { FeatureName, ProviderName, SettingsOfProvider } from './voidSettingsTypes.js'
import { IRange } from '../../../editor/common/core/range'
import { ProviderName, SettingsOfProvider } from './voidSettingsTypes.js'
export const errorDetails = (fullError: Error | null): string | null => {
if (fullError === null) {
return null
}
else if (typeof fullError === 'object') {
if (Object.keys(fullError).length === 0) return null
return JSON.stringify(fullError, null, 2)
}
else if (typeof fullError === 'string') {
return null
}
return null
}
export type OnText = (p: { newText: string, fullText: string }) => void
export type OnFinalMessage = (p: { fullText: string }) => void
export type OnError = (p: { message: string, fullError: Error | null }) => void
export type AbortRef = { current: (() => void) | null }
export type LLMChatMessage = {
export type LLMMessage = {
role: 'system' | 'user' | 'assistant';
content: string;
}
export type _InternalLLMChatMessage = {
role: 'user' | 'assistant';
content: string;
}
type _InternalSendFIMMessage = {
prefix: string;
suffix: string;
stopTokens: string[];
}
type SendLLMType = {
messagesType: 'chatMessages';
messages: LLMChatMessage[];
export type ServiceSendLLMFeatureParams = {
featureName: 'Ctrl+K';
range: IRange;
} | {
messagesType: 'FIMMessage';
messages: _InternalSendFIMMessage;
featureName: 'Ctrl+L';
} | {
featureName: 'Autocomplete';
range: IRange;
}
// params to the true sendLLMMessage function
export type LLMMMessageParams = {
onText: OnText;
onFinalMessage: OnFinalMessage;
onError: OnError;
abortRef: AbortRef;
messages: LLMMessage[];
logging: {
loggingName: string,
};
providerName: ProviderName;
modelName: string;
settingsOfProvider: SettingsOfProvider;
}
// service types
export type ServiceSendLLMMessageParams = {
onText: OnText;
onFinalMessage: OnFinalMessage;
onError: OnError;
logging: { loggingName: string, };
useProviderFor: FeatureName;
} & SendLLMType
// params to the true sendLLMMessage function
export type SendLLMMessageParams = {
onText: OnText;
onFinalMessage: OnFinalMessage;
onError: OnError;
logging: { loggingName: string, };
abortRef: AbortRef;
aiInstructions: string;
providerName: ProviderName;
modelName: string;
settingsOfProvider: SettingsOfProvider;
} & SendLLMType
messages: LLMMessage[];
logging: {
loggingName: string,
};
} & ServiceSendLLMFeatureParams
// can't send functions across a proxy, use listeners instead
export type BlockedMainLLMMessageParams = 'onText' | 'onFinalMessage' | 'onError' | 'abortRef'
export type MainSendLLMMessageParams = Omit<SendLLMMessageParams, BlockedMainLLMMessageParams> & { requestId: string } & SendLLMType
export type MainLLMMessageParams = Omit<LLMMMessageParams, BlockedMainLLMMessageParams> & { requestId: string }
export type MainLLMMessageAbortParams = { requestId: string }
export type EventLLMMessageOnTextParams = Parameters<OnText>[0] & { requestId: string }
export type EventLLMMessageOnFinalMessageParams = Parameters<OnFinalMessage>[0] & { requestId: string }
export type EventLLMMessageOnErrorParams = Parameters<OnError>[0] & { requestId: string }
export type _InternalSendLLMMessageFnType = (params: {
messages: LLMMessage[];
onText: OnText;
onFinalMessage: OnFinalMessage;
onError: OnError;
settingsOfProvider: SettingsOfProvider;
providerName: ProviderName;
modelName: string;
export type _InternalSendLLMChatMessageFnType = (
params: {
onText: OnText;
onFinalMessage: OnFinalMessage;
onError: OnError;
providerName: ProviderName;
settingsOfProvider: SettingsOfProvider;
modelName: string;
_setAborter: (aborter: () => void) => void;
messages: _InternalLLMChatMessage[];
}
) => void
export type _InternalSendLLMFIMMessageFnType = (
params: {
onText: OnText;
onFinalMessage: OnFinalMessage;
onError: OnError;
providerName: ProviderName;
settingsOfProvider: SettingsOfProvider;
modelName: string;
_setAborter: (aborter: () => void) => void;
messages: _InternalSendFIMMessage;
}
) => void
_setAborter: (aborter: () => void) => void;
}) => void
// service -> main -> internal -> event (back to main)
// (browser)
@@ -125,6 +89,13 @@ export type _InternalSendLLMFIMMessageFnType = (
// These are from 'ollama' SDK
interface OllamaModelDetails {
parent_model: string;
@@ -0,0 +1,39 @@
/*------------------------------------------------------------------------------------------
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
*-----------------------------------------------------------------------------------------*/
import { createDecorator } from '../../instantiation/common/instantiation.js';
import { ProxyChannel } from '../../../base/parts/ipc/common/ipc.js';
import { IMainProcessService } from '../../ipc/common/mainProcessService.js';
import { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js';
export interface IMetricsService {
readonly _serviceBrand: undefined;
capture(event: string, params: Record<string, any>): void;
}
export const IMetricsService = createDecorator<IMetricsService>('metricsService');
// implemented by calling channel
export class MetricsService implements IMetricsService {
readonly _serviceBrand: undefined;
private readonly metricsService: IMetricsService;
constructor(
@IMainProcessService mainProcessService: IMainProcessService // (only usable on client side)
) {
this.metricsService = ProxyChannel.toService<IMetricsService>(mainProcessService.getChannel('void-channel-metrics'));
}
// call capture on the channel
capture(...params: Parameters<IMetricsService['capture']>) {
this.metricsService.capture(...params);
}
}
registerSingleton(IMetricsService, MetricsService, InstantiationType.Eager);
@@ -1,16 +1,16 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
*-----------------------------------------------------------------------------------------*/
import { 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, IDisposable } from '../../../../base/common/lifecycle.js';
import { Emitter, Event } from '../../../base/common/event.js';
import { Disposable, IDisposable } from '../../../base/common/lifecycle.js';
import { RefreshableProviderName, refreshableProviderNames, SettingsOfProvider } from './voidSettingsTypes.js';
import { OllamaModelResponse, OpenaiCompatibleModelResponse } from './llmMessageTypes.js';
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
@@ -25,33 +25,22 @@ type RefreshableState = ({
state: 'finished',
timeoutId: null,
} | {
state: 'error',
state: 'finished_invisible',
timeoutId: null,
})
/*
user click -> error -> fire(error)
\> success -> fire(success)
finally: keep polling
poll -> do not fire
*/
export type RefreshModelStateOfProvider = Record<RefreshableProviderName, RefreshableState>
const refreshBasedOn: { [k in RefreshableProviderName]: (keyof SettingsOfProvider[k])[] } = {
ollama: ['_didFillInProviderSettings', 'endpoint'],
// openAICompatible: ['_didFillInProviderSettings', 'endpoint', 'apiKey'],
ollama: ['_enabled', 'endpoint'],
openAICompatible: ['_enabled', 'endpoint', 'apiKey'],
}
const REFRESH_INTERVAL = 5_000
// const COOLDOWN_TIMEOUT = 300
const autoOptions = { enableProviderOnSuccess: true, doNotFire: true }
// element-wise equals
function eq<T>(a: T[], b: T[]): boolean {
if (a.length !== b.length) return false
@@ -62,7 +51,7 @@ function eq<T>(a: T[], b: T[]): boolean {
}
export interface IRefreshModelService {
readonly _serviceBrand: undefined;
startRefreshingModels: (providerName: RefreshableProviderName, options: { enableProviderOnSuccess: boolean, doNotFire: boolean }) => void;
refreshModels: (providerName: RefreshableProviderName) => Promise<void>;
onDidChangeState: Event<RefreshableProviderName>;
state: RefreshModelStateOfProvider;
}
@@ -86,23 +75,23 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
const disposables: Set<IDisposable> = new Set()
const initializeAutoPollingAndOnChange = () => {
const initializePollingAndOnChange = () => {
this._clearAllTimeouts()
disposables.forEach(d => d.dispose())
disposables.clear()
if (!voidSettingsService.state.globalSettings.autoRefreshModels) return
if (!voidSettingsService.state.featureFlagSettings.autoRefreshModels) return
for (const providerName of refreshableProviderNames) {
// const { '_didFillInProviderSettings': enabled } = this.voidSettingsService.state.settingsOfProvider[providerName]
this.startRefreshingModels(providerName, autoOptions)
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 => voidSettingsService.state.settingsOfProvider[providerName][settingName])
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(
voidSettingsService.onDidChangeState(() => { // we might want to debounce this
this.voidSettingsService.onDidChangeState(() => { // we might want to debounce this
const newVals = relevantVals()
if (!eq(prevVals, newVals)) {
@@ -112,7 +101,7 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
// if it was just enabled, or there was a change and it wasn't to the enabled state, refresh
if ((enabled && !prevEnabled) || (!enabled && !prevEnabled)) {
// if user just clicked enable, refresh
this.startRefreshingModels(providerName, autoOptions)
this.refreshModels(providerName, !enabled, { isPolling: false, isInternal: true })
}
else {
// else if user just clicked disable, don't refresh
@@ -128,11 +117,11 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
}
}
// on mount (when get init settings state), and if a relevant feature flag changes, start refreshing models
// 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(() => {
initializeAutoPollingAndOnChange()
initializePollingAndOnChange()
this._register(
voidSettingsService.onDidChangeState((type) => { if (typeof type === 'object' && type[1] === 'autoRefreshModels') initializeAutoPollingAndOnChange() })
voidSettingsService.onDidChangeState((type) => { if (type === 'featureFlagSettings') initializePollingAndOnChange() })
)
})
@@ -140,53 +129,54 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
state: RefreshModelStateOfProvider = {
ollama: { state: 'init', timeoutId: null },
openAICompatible: { state: 'init', timeoutId: null },
}
// start listening for models (and don't stop until success)
startRefreshingModels: IRefreshModelService['startRefreshingModels'] = (providerName, options) => {
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)
this._setRefreshState(providerName, 'refreshing', options)
// start loading models
if (!isInternal) this._setRefreshState(providerName, 'refreshing')
const autoPoll = () => {
if (this.voidSettingsService.state.globalSettings.autoRefreshModels) {
// resume auto-polling
const timeoutId = setTimeout(() => this.startRefreshingModels(providerName, autoOptions), REFRESH_INTERVAL)
this._setTimeoutId(providerName, timeoutId)
}
}
const listFn = providerName === 'ollama' ? this.llmMessageService.ollamaList
const fn = providerName === 'ollama' ? this.llmMessageService.ollamaList
: providerName === 'openAICompatible' ? this.llmMessageService.openAICompatibleList
: () => { }
listFn({
fn({
onSuccess: ({ models }) => {
this.voidSettingsService.setDefaultModels(providerName, models.map(model => {
if (providerName === 'ollama') return (model as OllamaModelResponse).name
else if (providerName === 'openAICompatible') return (model as OpenaiCompatibleModelResponse).id
else throw new Error('refreshMode fn: unknown provider', providerName)
}))
// set the models to the detected models
this.voidSettingsService.setAutodetectedModels(
providerName,
models.map(model => {
if (providerName === 'ollama') return (model as OllamaModelResponse).name;
else if (providerName === 'openAICompatible') return (model as OpenaiCompatibleModelResponse).id;
else throw new Error('refreshMode fn: unknown provider', providerName);
}),
{ enableProviderOnSuccess: options.enableProviderOnSuccess, hideRefresh: options.doNotFire }
)
if (enableProviderOnSuccess) {
this.voidSettingsService.setSettingOfProvider(providerName, '_enabled', true)
}
if (options.enableProviderOnSuccess) this.voidSettingsService.setSettingOfProvider(providerName, '_didFillInProviderSettings', true)
if (!isInternal) this._setRefreshState(providerName, 'finished')
this._setRefreshState(providerName, 'finished', options)
autoPoll()
},
onError: ({ error }) => {
this._setRefreshState(providerName, 'error', options)
autoPoll()
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)
}
}
_clearAllTimeouts() {
@@ -207,8 +197,7 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
this.state[providerName].timeoutId = timeoutId
}
private _setRefreshState(providerName: RefreshableProviderName, state: RefreshableState['state'], options?: { doNotFire: boolean }) {
if (options?.doNotFire) return
private _setRefreshState(providerName: RefreshableProviderName, state: RefreshableState['state']) {
this.state[providerName].state = state
this._onDidChangeState.fire(providerName)
}
@@ -0,0 +1,268 @@
/*------------------------------------------------------------------------------------------
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
*-----------------------------------------------------------------------------------------*/
import { Emitter, Event } from '../../../base/common/event.js';
import { Disposable } from '../../../base/common/lifecycle.js';
import { deepClone } from '../../../base/common/objects.js';
import { IEncryptionService } from '../../encryption/common/encryptionService.js';
import { registerSingleton, InstantiationType } from '../../instantiation/common/extensions.js';
import { createDecorator } from '../../instantiation/common/instantiation.js';
import { IStorageService, StorageScope, StorageTarget } from '../../storage/common/storage.js';
import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, modelInfoOfDefaultNames, VoidModelInfo, FeatureFlagSettings, FeatureFlagName, defaultFeatureFlagSettings } from './voidSettingsTypes.js';
const STORAGE_KEY = 'void.voidSettingsStorage'
type SetSettingOfProviderFn = <S extends SettingName>(
providerName: ProviderName,
settingName: S,
newVal: SettingsOfProvider[ProviderName][S extends keyof SettingsOfProvider[ProviderName] ? S : never],
) => Promise<void>;
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 }
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; // in order to play nicely with react, you should immutably change state
readonly waitForInitState: Promise<void>;
onDidChangeState: Event<EventProp>;
setSettingOfProvider: SetSettingOfProviderFn;
setModelSelectionOfFeature: SetModelSelectionOfFeatureFn;
setFeatureFlag: SetFeatureFlagFn;
setDefaultModels(providerName: ProviderName, modelNames: string[]): void;
toggleModelHidden(providerName: ProviderName, modelName: string): void;
addModel(providerName: ProviderName, modelName: string): void;
deleteModel(providerName: ProviderName, modelName: string): boolean;
}
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
for (const { modelName, isHidden } of providerConfig.models) {
if (isHidden) continue
modelOptions.push({ text: `${modelName} (${providerName})`, value: { providerName, modelName } })
}
}
return modelOptions
}
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
}
export const IVoidSettingsService = createDecorator<IVoidSettingsService>('VoidSettingsService');
class VoidSettingsService extends Disposable implements IVoidSettingsService {
_serviceBrand: undefined;
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,
@IEncryptionService private readonly _encryptionService: IEncryptionService,
// could have used this, but it's clearer the way it is (+ slightly different eg StorageTarget.USER)
// @ISecretStorageService private readonly _secretStorageService: ISecretStorageService,
) {
super()
// 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
resolver()
this._onDidChangeState.fire('all')
})
}
private async _readState(): Promise<VoidSettingsState> {
const encryptedState = this._storageService.get(STORAGE_KEY, StorageScope.APPLICATION)
if (!encryptedState)
return defaultState()
const stateStr = await this._encryptionService.decrypt(encryptedState)
return JSON.parse(stateStr)
}
private async _storeState() {
const state = this.state
const encryptedState = await this._encryptionService.encrypt(JSON.stringify(state))
this._storageService.store(STORAGE_KEY, encryptedState, StorageScope.APPLICATION, StorageTarget.USER);
}
setSettingOfProvider: SetSettingOfProviderFn = async (providerName, settingName, newVal) => {
const newModelSelectionOfFeature = this.state.modelSelectionOfFeature
const newSettingsOfProvider = {
...this.state.settingsOfProvider,
[providerName]: {
...this.state.settingsOfProvider[providerName],
[settingName]: newVal,
}
}
const newFeatureFlags = this.state.featureFlagSettings
// if changed models or enabled a provider, recompute models list
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,
}
// this must go above this.setanythingelse()
this.state = newState
// if the user-selected model is no longer in the list, update the selection for each feature that needs it to something relevant (the 0th model available, or null)
if (modelsListChanged) {
for (const featureName of featureNames) {
const currentSelection = newModelSelectionOfFeature[featureName]
const selnIdx = currentSelection === null ? -1 : newModelsList.findIndex(m => modelSelectionsEqual(m.value, currentSelection))
if (selnIdx === -1) {
if (newModelsList.length !== 0)
this.setModelSelectionOfFeature(featureName, newModelsList[0].value, { doNotApplyEffects: true })
else
this.setModelSelectionOfFeature(featureName, null, { doNotApplyEffects: true })
}
}
}
await this._storeState()
this._onDidChangeState.fire('settingsOfProvider')
}
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: {
...this.state.modelSelectionOfFeature,
[featureName]: newVal
}
}
this.state = newState
if (options?.doNotApplyEffects)
return
await this._storeState()
this._onDidChangeState.fire('modelSelectionOfFeature')
}
setDefaultModels(providerName: ProviderName, newDefaultModelNames: string[]) {
const { models } = this.state.settingsOfProvider[providerName]
const newDefaultModels = modelInfoOfDefaultNames(newDefaultModelNames, { isAutodetected: true, existingModels: models })
const newModels = [
...newDefaultModels,
...models.filter(m => !m.isDefault), // keep any non-default models
]
this.setSettingOfProvider(providerName, 'models', newModels)
}
toggleModelHidden(providerName: ProviderName, modelName: string) {
const { models } = this.state.settingsOfProvider[providerName]
const modelIdx = models.findIndex(m => m.modelName === modelName)
if (modelIdx === -1) return
const newModels: VoidModelInfo[] = [
...models.slice(0, modelIdx),
{ ...models[modelIdx], isHidden: !models[modelIdx].isHidden },
...models.slice(modelIdx + 1, Infinity)
]
this.setSettingOfProvider(providerName, 'models', newModels)
}
addModel(providerName: ProviderName, modelName: string) {
const { models } = this.state.settingsOfProvider[providerName]
const existingIdx = models.findIndex(m => m.modelName === modelName)
if (existingIdx !== -1) return // if exists, do nothing
const newModels = [
...models,
{ modelName, isDefault: false, isHidden: false }
]
this.setSettingOfProvider(providerName, 'models', newModels)
}
deleteModel(providerName: ProviderName, modelName: string): boolean {
const { models } = this.state.settingsOfProvider[providerName]
const delIdx = models.findIndex(m => m.modelName === modelName)
if (delIdx === -1) return false
const newModels = [
...models.slice(0, delIdx), // delete the idx
...models.slice(delIdx + 1, Infinity)
]
this.setSettingOfProvider(providerName, 'models', newModels)
return true
}
}
registerSingleton(IVoidSettingsService, VoidSettingsService, InstantiationType.Eager);
@@ -0,0 +1,414 @@
/*------------------------------------------------------------------------------------------
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
*-----------------------------------------------------------------------------------------*/
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[] => {
const { isAutodetected, existingModels } = options ?? {}
const isDefault = true
const isHidden = modelNames.length >= 10 // hide all models if there are a ton of them, and make user enable them individually
if (!existingModels) {
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
export const defaultAnthropicModels = modelInfoOfDefaultNames([
'claude-3-5-sonnet-20241022',
'claude-3-5-haiku-20241022',
'claude-3-opus-20240229',
'claude-3-sonnet-20240229',
// 'claude-3-haiku-20240307',
])
// https://platform.openai.com/docs/models/gp
export const defaultOpenAIModels = modelInfoOfDefaultNames([
'o1-preview',
'o1-mini',
'gpt-4o',
'gpt-4o-mini',
// 'gpt-4o-2024-05-13',
// 'gpt-4o-2024-08-06',
// 'gpt-4o-mini-2024-07-18',
// 'gpt-4-turbo',
// 'gpt-4-turbo-2024-04-09',
// 'gpt-4-turbo-preview',
// 'gpt-4-0125-preview',
// 'gpt-4-1106-preview',
// 'gpt-4',
// 'gpt-4-0613',
// 'gpt-3.5-turbo-0125',
// 'gpt-3.5-turbo',
// 'gpt-3.5-turbo-1106',
])
// https://console.groq.com/docs/models
export const defaultGroqModels = modelInfoOfDefaultNames([
"mixtral-8x7b-32768",
"llama2-70b-4096",
"gemma-7b-it"
])
export const defaultGeminiModels = modelInfoOfDefaultNames([
'gemini-1.5-flash',
'gemini-1.5-pro',
'gemini-1.5-flash-8b',
'gemini-1.0-pro'
])
// export const parseMaxTokensStr = (maxTokensStr: string) => {
// // parse the string but only if the full string is a valid number, eg parseInt('100abc') should return NaN
// const int = isNaN(Number(maxTokensStr)) ? undefined : parseInt(maxTokensStr)
// if (Number.isNaN(int))
// return undefined
// return int
// }
export const anthropicMaxPossibleTokens = (modelName: string) => {
if (modelName === 'claude-3-5-sonnet-20241022'
|| modelName === 'claude-3-5-haiku-20241022')
return 8192
if (modelName === 'claude-3-opus-20240229'
|| modelName === 'claude-3-sonnet-20240229'
|| modelName === 'claude-3-haiku-20240307')
return 4096
return 1024 // return a reasonably small number if they're using a different model
}
type UnionOfKeys<T> = T extends T ? keyof T : never;
export const defaultProviderSettings = {
anthropic: {
apiKey: '',
},
openAI: {
apiKey: '',
},
ollama: {
endpoint: 'http://127.0.0.1:11434',
},
openRouter: {
apiKey: '',
},
openAICompatible: {
endpoint: '',
apiKey: '',
},
gemini: {
apiKey: '',
},
groq: {
apiKey: ''
}
} as const
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 defaultProviderSettings[ProviderName]>
type CustomProviderSettings<providerName extends ProviderName> = {
[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 | undefined, // undefined initially, computed when user types in all fields
models: VoidModelInfo[],
}
export type SettingsForProvider<providerName extends ProviderName> = CustomProviderSettings<providerName> & CommonProviderSettings
// part of state
export type SettingsOfProvider = {
[providerName in ProviderName]: SettingsForProvider<providerName>
}
export type SettingName = keyof SettingsForProvider<ProviderName>
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}"`)
}
type DisplayInfo = {
title: string,
placeholder: string,
subTextMd?: string,
}
export const displayInfoOfSettingName = (providerName: ProviderName, settingName: SettingName): DisplayInfo => {
if (settingName === 'apiKey') {
return {
title: 'API Key',
placeholder: providerName === 'anthropic' ? 'sk-ant-key...' : // sk-ant-api03-key
providerName === 'openAI' ? 'sk-proj-key...' :
providerName === 'openRouter' ? 'sk-or-key...' : // sk-or-v1-key
providerName === 'gemini' ? 'key...' :
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' ? 'Endpoint' :
providerName === 'openAICompatible' ? 'baseURL' // (do not include /chat/completions)
: '(never)',
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') {
return {
title: '(never)',
placeholder: '(never)',
}
}
else if (settingName === 'models') {
return {
title: '(never)',
placeholder: '(never)',
}
}
throw new Error(`displayInfo: Unknown setting name: "${settingName}"`)
}
const defaultCustomSettings: Record<CustomSettingName, undefined> = {
apiKey: undefined,
endpoint: undefined,
}
export const voidInitModelOptions = {
anthropic: {
models: defaultAnthropicModels,
},
openAI: {
models: defaultOpenAIModels,
},
ollama: {
models: [],
},
openRouter: {
models: [], // any string
},
openAICompatible: {
models: [],
},
gemini: {
models: defaultGeminiModels,
},
groq: {
models: defaultGroqModels,
},
}
// used when waiting and for a type reference
export const defaultSettingsOfProvider: SettingsOfProvider = {
anthropic: {
_enabled: undefined,
...defaultCustomSettings,
...defaultProviderSettings.anthropic,
...voidInitModelOptions.anthropic,
},
openAI: {
_enabled: undefined,
...defaultCustomSettings,
...defaultProviderSettings.openAI,
...voidInitModelOptions.openAI,
},
gemini: {
...defaultCustomSettings,
...defaultProviderSettings.gemini,
...voidInitModelOptions.gemini,
_enabled: undefined,
},
groq: {
...defaultCustomSettings,
...defaultProviderSettings.groq,
...voidInitModelOptions.groq,
_enabled: undefined,
},
ollama: {
...defaultCustomSettings,
...defaultProviderSettings.ollama,
...voidInitModelOptions.ollama,
_enabled: undefined,
},
openRouter: {
...defaultCustomSettings,
...defaultProviderSettings.openRouter,
...voidInitModelOptions.openRouter,
_enabled: undefined,
},
openAICompatible: {
...defaultCustomSettings,
...defaultProviderSettings.openAICompatible,
...voidInitModelOptions.openAICompatible,
_enabled: undefined,
},
}
export type ModelSelection = { providerName: ProviderName, modelName: string }
export const modelSelectionsEqual = (m1: ModelSelection, m2: ModelSelection) => {
return m1.modelName === m2.modelName && m1.providerName === m2.providerName
}
// this is a state
export type ModelSelectionOfFeature = {
'Ctrl+L': ModelSelection | null,
'Ctrl+K': ModelSelection | null,
'Autocomplete': ModelSelection | null,
}
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,34 +1,18 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
*-----------------------------------------------------------------------------------------*/
import Anthropic from '@anthropic-ai/sdk';
import { _InternalSendLLMChatMessageFnType } from '../../common/llmMessageTypes.js';
import { _InternalSendLLMMessageFnType } from '../../common/llmMessageTypes.js';
import { anthropicMaxPossibleTokens } from '../../common/voidSettingsTypes.js';
import { InternalToolInfo } from '../../common/toolsService.js';
export const toAnthropicTool = (toolName: string, toolInfo: InternalToolInfo) => {
const { description, params, required } = toolInfo
return {
name: toolName,
description: description,
input_schema: {
type: 'object',
properties: params,
required: required,
}
} satisfies Anthropic.Messages.Tool
// Anthropic
type LLMMessageAnthropic = {
role: 'user' | 'assistant';
content: string;
}
export const sendAnthropicChat: _InternalSendLLMChatMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => {
export const sendAnthropicMsg: _InternalSendLLMMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => {
const thisConfig = settingsOfProvider.anthropic
@@ -40,9 +24,20 @@ export const sendAnthropicChat: _InternalSendLLMChatMessageFnType = ({ messages,
const anthropic = new Anthropic({ apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true });
// find system messages and concatenate them
const systemMessage = messages
.filter(msg => msg.role === 'system')
.map(msg => msg.content)
.join('\n');
// remove system messages for Anthropic
const anthropicMessages = messages.filter(msg => msg.role !== 'system') as LLMMessageAnthropic[]
const stream = anthropic.messages.stream({
// system: systemMessage,
messages: messages,
system: systemMessage,
messages: anthropicMessages,
model: modelName,
max_tokens: maxTokens,
});
@@ -0,0 +1,53 @@
/*------------------------------------------------------------------------------------------
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
*-----------------------------------------------------------------------------------------*/
import { Content, GoogleGenerativeAI, GoogleGenerativeAIFetchError } from '@google/generative-ai';
import { _InternalSendLLMMessageFnType } from '../../common/llmMessageTypes.js';
// Gemini
export const sendGeminiMsg: _InternalSendLLMMessageFnType = async ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => {
let fullText = ''
const thisConfig = settingsOfProvider.gemini
const genAI = new GoogleGenerativeAI(thisConfig.apiKey);
const model = genAI.getGenerativeModel({ model: modelName });
// remove system messages that get sent to Gemini
// str of all system messages
const systemMessage = messages
.filter(msg => msg.role === 'system')
.map(msg => msg.content)
.join('\n');
// Convert messages to Gemini format
const geminiMessages: Content[] = messages
.filter(msg => msg.role !== 'system')
.map((msg, i) => ({
parts: [{ text: msg.content }],
role: msg.role === 'assistant' ? 'model' : 'user'
}))
model.generateContentStream({ contents: geminiMessages, systemInstruction: systemMessage, })
.then(async response => {
_setAborter(() => response.stream.return(fullText))
for await (const chunk of response.stream) {
const newText = chunk.text();
fullText += newText;
onText({ newText, fullText });
}
onFinalMessage({ fullText });
})
.catch((error) => {
if (error instanceof GoogleGenerativeAIFetchError && error.status === 400) {
onError({ message: 'Invalid API key.', fullError: null });
}
else {
onError({ message: error + '', fullError: error });
}
})
}
@@ -0,0 +1,68 @@
/*------------------------------------------------------------------------------------------
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
*-----------------------------------------------------------------------------------------*/
// // Greptile
// // https://docs.greptile.com/api-reference/query
// // https://docs.greptile.com/quickstart#sample-response-streamed
// import { SendLLMMessageFnTypeInternal } from '../../common/llmMessageTypes.js';
// export const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, _setAborter }) => {
// let fullText = ''
// const thisConfig = settingsOfProvider.greptile
// fetch('https://api.greptile.com/v2/query', {
// method: 'POST',
// headers: {
// 'Authorization': `Bearer ${thisConfig.apikey}`,
// 'X-Github-Token': `${thisConfig.githubPAT}`,
// 'Content-Type': `application/json`,
// },
// body: JSON.stringify({
// messages,
// stream: true,
// repositories: [thisConfig.repoinfo],
// }),
// })
// // this is {message}\n{message}\n{message}...\n
// .then(async response => {
// const text = await response.text()
// console.log('got greptile', text)
// return JSON.parse(`[${text.trim().split('\n').join(',')}]`)
// })
// // TODO make this actually stream, right now it just sends one message at the end
// // TODO add _setAborter() when add streaming
// .then(async responseArr => {
// for (const response of responseArr) {
// const type: string = response['type']
// const message = response['message']
// // when receive text
// if (type === 'message') {
// fullText += message
// onText({ newText: message, fullText })
// }
// else if (type === 'sources') {
// const { filepath, linestart: _, lineend: _2 } = message as { filepath: string; linestart: number | null; lineend: number | null }
// fullText += filepath
// onText({ newText: filepath, fullText })
// }
// // type: 'status' with an empty 'message' means last message
// else if (type === 'status') {
// if (!message) {
// onFinalMessage({ fullText })
// }
// }
// }
// })
// .catch(error => {
// onError({ error })
// });
// }
@@ -1,13 +1,13 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
*-----------------------------------------------------------------------------------------*/
import Groq from 'groq-sdk';
import { _InternalSendLLMChatMessageFnType } from '../../common/llmMessageTypes.js';
import { _InternalSendLLMMessageFnType } from '../../common/llmMessageTypes.js';
// Groq
export const sendGroqChat: _InternalSendLLMChatMessageFnType = async ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => {
export const sendGroqMsg: _InternalSendLLMMessageFnType = async ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => {
let fullText = '';
const thisConfig = settingsOfProvider.groq
@@ -22,14 +22,18 @@ export const sendGroqChat: _InternalSendLLMChatMessageFnType = async ({ messages
messages: messages,
model: modelName,
stream: true,
temperature: 0.7,
// max_tokens: parseMaxTokensStr(thisConfig.maxTokens),
})
.then(async response => {
_setAborter(() => response.controller.abort())
// when receive text
for await (const chunk of response) {
const newText = chunk.choices[0]?.delta?.content || '';
fullText += newText;
onText({ newText, fullText });
if (newText) {
fullText += newText;
onText({ newText, fullText });
}
}
onFinalMessage({ fullText });
@@ -1,10 +1,10 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
*-----------------------------------------------------------------------------------------*/
import { Ollama } from 'ollama';
import { _InternalModelListFnType, _InternalSendLLMFIMMessageFnType, _InternalSendLLMChatMessageFnType, OllamaModelResponse } from '../../common/llmMessageTypes.js';
import { _InternalModelListFnType, _InternalSendLLMMessageFnType, OllamaModelResponse } from '../../common/llmMessageTypes.js';
import { defaultProviderSettings } from '../../common/voidSettingsTypes.js';
export const ollamaList: _InternalModelListFnType<OllamaModelResponse> = async ({ onSuccess: onSuccess_, onError: onError_, settingsOfProvider }) => {
@@ -38,47 +38,8 @@ export const ollamaList: _InternalModelListFnType<OllamaModelResponse> = async (
}
export const sendOllamaFIM: _InternalSendLLMFIMMessageFnType = ({ 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 = ''
const ollama = new Ollama({ host: thisConfig.endpoint })
ollama.generate({
model: modelName,
prompt: messages.prefix,
suffix: messages.suffix,
options: {
stop: messages.stopTokens,
num_predict: 300, // max tokens
// repeat_penalty: 1,
},
raw: true,
stream: true,
})
.then(async stream => {
_setAborter(() => stream.abort())
// iterate through the stream
for await (const chunk of stream) {
const newText = chunk.response;
fullText += newText;
onText({ newText, fullText });
}
onFinalMessage({ fullText });
})
// when error/fail
.catch((error) => {
onError({ message: error + '', fullError: error })
})
};
// Ollama
export const sendOllamaChat: _InternalSendLLMChatMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => {
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
@@ -107,6 +68,14 @@ export const sendOllamaChat: _InternalSendLLMChatMessageFnType = ({ messages, on
})
// when error/fail
.catch((error) => {
// if (typeof error === 'object') {
// const e = error.error as ErrorResponse['error']
// if (e) {
// const name = error.name ?? 'Error'
// onError({ error: `${name}: ${e}` })
// return;
// }
// }
onError({ message: error + '', fullError: error })
})
@@ -0,0 +1,105 @@
/*------------------------------------------------------------------------------------------
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
*-----------------------------------------------------------------------------------------*/
import OpenAI from 'openai';
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 }) => {
let fullText = ''
let openai: OpenAI
let options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming
if (providerName === 'openAI') {
const thisConfig = settingsOfProvider.openAI
openai = new OpenAI({ apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true });
options = { model: modelName, messages: messages, stream: true, /*max_completion_tokens: parseMaxTokensStr(thisConfig.maxTokens)*/ }
}
else if (providerName === 'openRouter') {
const thisConfig = settingsOfProvider.openRouter
openai = new OpenAI({
baseURL: 'https://openrouter.ai/api/v1', apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true,
defaultHeaders: {
'HTTP-Referer': 'https://voideditor.com', // Optional, for including your app on openrouter.ai rankings.
'X-Title': 'Void Editor', // Optional. Shows in rankings on openrouter.ai.
},
});
options = { model: modelName, messages: messages, stream: true, /*max_completion_tokens: parseMaxTokensStr(thisConfig.maxTokens)*/ }
}
else if (providerName === 'openAICompatible') {
const thisConfig = settingsOfProvider.openAICompatible
openai = new OpenAI({ baseURL: thisConfig.endpoint, apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true })
options = { model: modelName, messages: messages, stream: true, /*max_completion_tokens: parseMaxTokensStr(thisConfig.maxTokens)*/ }
}
else {
console.error(`sendOpenAIMsg: invalid providerName: ${providerName}`)
throw new Error(`providerName was invalid: ${providerName}`)
}
openai.models.list()
openai.chat.completions
.create(options)
.then(async response => {
_setAborter(() => response.controller.abort())
// when receive text
for await (const chunk of response) {
const newText = chunk.choices[0]?.delta?.content || '';
fullText += newText;
onText({ newText, fullText });
}
onFinalMessage({ fullText });
})
// when error/fail - this catches errors of both .create() and .then(for await)
.catch(error => {
if (error instanceof OpenAI.APIError && error.status === 401) {
onError({ message: 'Invalid API key.', fullError: error });
}
else {
onError({ message: error, fullError: error });
}
})
};
@@ -0,0 +1,115 @@
/*------------------------------------------------------------------------------------------
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
*-----------------------------------------------------------------------------------------*/
import { LLMMMessageParams, OnText, OnFinalMessage, OnError } from '../../common/llmMessageTypes.js';
import { IMetricsService } from '../../common/metricsService.js';
import { sendAnthropicMsg } from './anthropic.js';
import { sendOllamaMsg } from './ollama.js';
import { sendOpenAIMsg } from './openai.js';
import { sendGeminiMsg } from './gemini.js';
import { sendGroqMsg } from './groq.js';
export const sendLLMMessage = ({
messages,
onText: onText_,
onFinalMessage: onFinalMessage_,
onError: onError_,
abortRef: abortRef_,
logging: { loggingName },
settingsOfProvider,
providerName,
modelName,
}: LLMMMessageParams,
metricsService: IMetricsService
) => {
// trim message content (Anthropic and other providers give an error if there is trailing whitespace)
messages = messages.map(m => ({ ...m, content: m.content.trim() }))
// only captures number of messages and message "shape", no actual code, instructions, prompts, etc
const captureChatEvent = (eventId: string, extras?: object) => {
metricsService.capture(eventId, {
providerName,
numMessages: messages?.length,
messagesShape: messages?.map(msg => ({ role: msg.role, length: msg.content.length })),
version: '2024-11-14',
...extras,
})
}
const submit_time = new Date()
let _fullTextSoFar = ''
let _aborter: (() => void) | null = null
let _setAborter = (fn: () => void) => { _aborter = fn }
let _didAbort = false
const onText: OnText = ({ newText, fullText }) => {
if (_didAbort) return
onText_({ newText, fullText })
_fullTextSoFar = fullText
}
const onFinalMessage: OnFinalMessage = ({ fullText }) => {
if (_didAbort) return
captureChatEvent(`${loggingName} - Received Full Message`, { messageLength: fullText.length, duration: new Date().getMilliseconds() - submit_time.getMilliseconds() })
onFinalMessage_({ fullText })
}
const onError: OnError = ({ message: error, fullError }) => {
if (_didAbort) return
console.log("ERROR!!!!!", error)
console.error('sendLLMMessage onError:', error)
captureChatEvent(`${loggingName} - Error`, { error })
onError_({ message: error, fullError })
}
const onAbort = () => {
captureChatEvent(`${loggingName} - Abort`, { messageLengthSoFar: _fullTextSoFar.length })
try { _aborter?.() } // aborter sometimes automatically throws an error
catch (e) { }
_didAbort = true
}
abortRef_.current = onAbort
captureChatEvent(`${loggingName} - Sending Message`, { messageLength: messages[messages.length - 1]?.content.length })
try {
switch (providerName) {
case 'anthropic':
sendAnthropicMsg({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
break;
case 'openAI':
case 'openRouter':
case 'openAICompatible':
sendOpenAIMsg({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
break;
case 'gemini':
sendGeminiMsg({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
break;
case 'ollama':
sendOllamaMsg({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
break;
case 'groq':
sendGroqMsg({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
break;
default:
onError({ message: `Error: Void provider was "${providerName}", which is not recognized.`, fullError: null })
break;
}
}
catch (error) {
if (error instanceof Error) { onError({ message: error + '', fullError: error }) }
else { onError({ message: `Unexpected Error in sendLLMMessage: ${error}`, fullError: error }); }
// ; (_aborter as any)?.()
// _didAbort = true
}
}
@@ -1,14 +1,14 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the MIT License. See LICENSE.txt in the project root 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, MainSendLLMMessageParams, AbortRef, SendLLMMessageParams, MainLLMMessageAbortParams, MainModelListParams, ModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, OllamaModelResponse, OpenaiCompatibleModelResponse, } from '../common/llmMessageTypes.js';
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, 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';
@@ -66,7 +66,7 @@ export class LLMMessageChannel implements IServerChannel {
}
}
// browser uses this to call (see this.channel.call() in llmMessageService.ts for all usages)
// browser uses this to call
async call(_: unknown, command: string, params: any): Promise<any> {
try {
if (command === 'sendLLMMessage') {
@@ -91,13 +91,13 @@ export class LLMMessageChannel implements IServerChannel {
}
// the only place sendLLMMessage is actually called
private async _callSendLLMMessage(params: MainSendLLMMessageParams) {
private async _callSendLLMMessage(params: MainLLMMessageParams) {
const { requestId } = params;
if (!(requestId in this._abortRefOfRequestId_llm))
this._abortRefOfRequestId_llm[requestId] = { current: null }
const mainThreadParams: SendLLMMessageParams = {
const mainThreadParams: LLMMMessageParams = {
...params,
onText: ({ newText, fullText }) => { this._onText_llm.fire({ requestId, newText, fullText }); },
onFinalMessage: ({ fullText }) => { this._onFinalMessage_llm.fire({ requestId, fullText }); },
@@ -0,0 +1,46 @@
/*------------------------------------------------------------------------------------------
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
*-----------------------------------------------------------------------------------------*/
import { Disposable } from '../../../base/common/lifecycle.js';
import { ITelemetryService } from '../../telemetry/common/telemetry.js';
import { IMetricsService } from '../common/metricsService.js';
import { PostHog } from 'posthog-node'
// posthog-js (old):
// posthog.init('phc_UanIdujHiLp55BkUTjB1AuBXcasVkdqRwgnwRlWESH2', { api_host: 'https://us.i.posthog.com', })
// const buildEnv = 'development';
// const buildNumber = '1.0.0';
// const isMac = process.platform === 'darwin';
export class MetricsMainService extends Disposable implements IMetricsService {
_serviceBrand: undefined;
readonly _distinctId: string
readonly client: PostHog
constructor(
@ITelemetryService private readonly _telemetryService: ITelemetryService
) {
super()
this.client = new PostHog('phc_UanIdujHiLp55BkUTjB1AuBXcasVkdqRwgnwRlWESH2', { host: 'https://us.i.posthog.com', })
const { devDeviceId, firstSessionDate, machineId } = this._telemetryService
this._distinctId = devDeviceId
this.client.identify({ distinctId: devDeviceId, properties: { firstSessionDate, machineId } })
console.log('Void posthog metrics info:', JSON.stringify({ devDeviceId, firstSessionDate, machineId }))
}
capture: IMetricsService['capture'] = (event, params) => {
const capture = { distinctId: this._distinctId, event, properties: params } as const
// console.log('full capture:', capture)
this.client.capture(capture)
}
}
@@ -14,8 +14,6 @@ import { WorkspaceEdit } from '../../../editor/common/languages.js';
// import { IHistoryService } from '../../services/history/common/history.js';
// VOID: THIS FILE IS OUTDATED!!!!!! No longer used anywhere.
@extHostNamedCustomer(MainContext.MainThreadInlineDiff)
export class MainThreadInlineDiff extends Disposable implements MainThreadInlineDiffShape {
+1 -1
View File
@@ -2534,7 +2534,7 @@ const LayoutStateKeys = {
// Part Sizing
GRID_SIZE: new InitializationStateKey('grid.size', StorageScope.PROFILE, StorageTarget.MACHINE, { width: 800, height: 600 }),
SIDEBAR_SIZE: new InitializationStateKey<number>('sideBar.size', StorageScope.PROFILE, StorageTarget.MACHINE, 200),
AUXILIARYBAR_SIZE: new InitializationStateKey<number>('auxiliaryBar.size', StorageScope.PROFILE, StorageTarget.MACHINE, 800), // Void changed this from 200 to 800
AUXILIARYBAR_SIZE: new InitializationStateKey<number>('auxiliaryBar.size', StorageScope.PROFILE, StorageTarget.MACHINE, 200),
PANEL_SIZE: new InitializationStateKey<number>('panel.size', StorageScope.PROFILE, StorageTarget.MACHINE, 300),
PANEL_LAST_NON_MAXIMIZED_HEIGHT: new RuntimeStateKey<number>('panel.lastNonMaximizedHeight', StorageScope.PROFILE, StorageTarget.MACHINE, 300),
@@ -0,0 +1 @@
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><style>.st0{fill:#f6f6f6;fill-opacity:0}.st1{fill:#fff}.st2{fill:#167abf}</style><path class="st0" d="M1024 1024H0V0h1024v1024z"/><path class="st1" d="M1024 85.333v853.333H0V85.333h1024z"/><path class="st2" d="M0 85.333h298.667v853.333H0V85.333zm1024 0v853.333H384V85.333h640zm-554.667 160h341.333v-64H469.333v64zm341.334 533.334H469.333v64h341.333l.001-64zm128-149.334H597.333v64h341.333l.001-64zm0-149.333H597.333v64h341.333l.001-64zm0-149.333H597.333v64h341.333l.001-64z"/></svg>

After

Width:  |  Height:  |  Size: 559 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 795 KiB

@@ -45,7 +45,7 @@ export class AuxiliaryBarPart extends AbstractPaneCompositePart {
static readonly viewContainersWorkspaceStateKey = 'workbench.auxiliarybar.viewContainersWorkspaceState';
// Use the side bar dimensions
override readonly minimumWidth: number = 230; // Void changed this (was 170)
override readonly minimumWidth: number = 170;
override readonly maximumWidth: number = Number.POSITIVE_INFINITY;
override readonly minimumHeight: number = 0;
override readonly maximumHeight: number = Number.POSITIVE_INFINITY;
@@ -30,7 +30,7 @@
background-repeat: no-repeat;
background-position: center center;
background-size: 16px;
background-image: url('../../../../browser/media/void-icon-sm.png');
background-image: url('../../../../browser/media/code-icon.svg');
width: 16px;
padding: 0;
margin: 0 6px 0 10px;
@@ -432,7 +432,7 @@ export class UnpinEditorAction extends Action {
label: string,
@ICommandService private readonly commandService: ICommandService
) {
super(id, label, ThemeIcon.asClassName(Codicon.starFull));
super(id, label, ThemeIcon.asClassName(Codicon.pinned));
}
override run(context?: IEditorCommandsContext): Promise<void> {
@@ -440,24 +440,6 @@ export class UnpinEditorAction extends Action {
}
}
export class PinEditorAction extends Action {
static readonly ID = 'workbench.action.pinEditor';
static readonly LABEL = localize('pinEditor', "Pin Editor");
constructor(
id: string,
label: string,
@ICommandService private readonly commandService: ICommandService
) {
super(id, label, ThemeIcon.asClassName(Codicon.star));
}
override async run(context?: IEditorCommandsContext): Promise<void> {
return this.commandService.executeCommand('workbench.action.pinEditor', undefined, context);
}
}
export class CloseEditorTabAction extends Action {
static readonly ID = 'workbench.action.closeActiveEditor';
@@ -18,13 +18,14 @@ import { isRecentFolder, IWorkspacesService } from '../../../../platform/workspa
import { ICommandService } from '../../../../platform/commands/common/commands.js';
import { OpenFileFolderAction, OpenFolderAction } from '../../actions/workspaceActions.js';
import { isMacintosh, isNative, OS } from '../../../../base/common/platform.js';
import { VOID_CTRL_L_ACTION_ID } from '../../../contrib/void/browser/sidebarActions.js';
import { VOID_CTRL_K_ACTION_ID } from '../../../contrib/void/browser/quickEditActions.js';
import { defaultKeybindingLabelStyles } from '../../../../platform/theme/browser/defaultStyles.js';
import { IWindowOpenable } from '../../../../platform/window/common/window.js';
import { ILabelService, Verbosity } from '../../../../platform/label/common/label.js';
import { splitRecentLabel } from '../../../../base/common/labels.js';
import { IHostService } from '../../../services/host/browser/host.js';
import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../contrib/void/browser/voidSettingsPane.js';
import { VOID_CTRL_K_ACTION_ID, VOID_CTRL_L_ACTION_ID } from '../../../contrib/void/browser/actionIDs.js';
// import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
registerColor('editorWatermark.foreground', { dark: transparent(editorForeground, 0.6), light: transparent(editorForeground, 0.68), hcDark: editorForeground, hcLight: editorForeground }, localize('editorLineHighlight', 'Foreground color for the labels in the editor watermark.'));
@@ -146,23 +147,38 @@ export class EditorGroupWatermark extends Disposable {
private render(): void {
// const enabled = this.configurationService.getValue<boolean>('workbench.tips.enabled');
// 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.clear();
const voidIconBox = append(this.shortcuts, $('.watermark-box'));
const recentsBox = append(this.shortcuts, $('div'));
recentsBox.style.display = 'flex'
recentsBox.style.flex = 'row'
recentsBox.style.justifyContent = 'center'
const box = append(this.shortcuts, $('.watermark-box'));
const boxBelow = append(this.shortcuts, $(''))
boxBelow.style.display = 'flex'
boxBelow.style.flex = 'row'
boxBelow.style.justifyContent = 'center'
const update = async () => {
// put async at top so don't need to wait (this prevents a jitter on load)
const recentlyOpened = await this.workspacesService.getRecentlyOpened()
.catch(() => ({ files: [], workspaces: [] })).then(w => w.workspaces);
clearNode(voidIconBox);
clearNode(recentsBox);
clearNode(box);
clearNode(boxBelow);
this.currentDisposables.forEach(label => label.dispose());
this.currentDisposables.clear();
@@ -172,14 +188,13 @@ export class EditorGroupWatermark extends Disposable {
if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) {
// Open a folder
const openFolderButton = h('button')
openFolderButton.root.classList.add('void-watermark-button')
openFolderButton.root.style.display = 'block'
openFolderButton.root.style.marginLeft = 'auto'
openFolderButton.root.style.marginRight = 'auto'
openFolderButton.root.style.marginBottom = '16px'
openFolderButton.root.textContent = 'Open a folder'
openFolderButton.root.onclick = () => {
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);
@@ -187,14 +202,23 @@ export class EditorGroupWatermark extends Disposable {
// this.commandService.executeCommand(isMacintosh ? 'workbench.action.files.openFileFolder' : 'workbench.action.files.openFolder');
// }
}
voidIconBox.appendChild(openFolderButton.root);
box.appendChild(button.root);
// Recents
const recentlyOpened = await this.workspacesService.getRecentlyOpened()
.catch(() => ({ files: [], workspaces: [] })).then(w => w.workspaces);
if (recentlyOpened.length !== 0) {
voidIconBox.append(
...recentlyOpened.map((w, i) => {
const span = $('div')
span.textContent = 'Recent'
span.style.fontWeight = '500'
box.append(span)
box.append(
...recentlyOpened.map(w => {
let fullPath: string;
let windowOpenable: IWindowOpenable;
@@ -211,13 +235,14 @@ export class EditorGroupWatermark extends Disposable {
const { name, parentPath } = splitRecentLabel(fullPath);
const linkSpan = $('span');
linkSpan.classList.add('void-link')
linkSpan.style.display = 'flex'
linkSpan.style.gap = '4px'
linkSpan.style.padding = '8px'
const li = $('li');
const link = $('span');
link.classList.add('void-link')
linkSpan.addEventListener('click', e => {
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
@@ -225,30 +250,29 @@ export class EditorGroupWatermark extends Disposable {
e.preventDefault();
e.stopPropagation();
});
li.appendChild(link);
const nameSpan = $('span');
nameSpan.innerText = name;
nameSpan.title = fullPath;
linkSpan.appendChild(nameSpan);
const span = $('span');
span.style.paddingLeft = '4px';
span.classList.add('path');
span.classList.add('detail');
span.innerText = parentPath;
span.title = fullPath;
li.appendChild(span);
const dirSpan = $('span');
dirSpan.style.paddingLeft = '4px';
dirSpan.innerText = parentPath;
dirSpan.title = fullPath;
linkSpan.appendChild(dirSpan);
return linkSpan
return li
}).filter(v => !!v)
)
}
}
else {
// show them Void keybindings
const keys = this.keybindingService.lookupKeybinding(VOID_CTRL_L_ACTION_ID);
const dl = append(voidIconBox, $('dl'));
const dl = append(box, $('dl'));
const dt = append(dl, $('dt'));
dt.textContent = 'Chat'
const dd = append(dl, $('dd'));
@@ -259,7 +283,7 @@ export class EditorGroupWatermark extends Disposable {
const keys2 = this.keybindingService.lookupKeybinding(VOID_CTRL_K_ACTION_ID);
const dl2 = append(voidIconBox, $('dl'));
const dl2 = append(box, $('dl'));
const dt2 = append(dl2, $('dt'));
dt2.textContent = 'Quick Edit'
const dd2 = append(dl2, $('dd'));
@@ -269,7 +293,7 @@ export class EditorGroupWatermark extends Disposable {
this.currentDisposables.add(label2);
const keys3 = this.keybindingService.lookupKeybinding('workbench.action.openGlobalKeybindings');
const button3 = append(recentsBox, $('button'));
const button3 = append(boxBelow, $('button'));
button3.textContent = 'Void Settings'
button3.style.display = 'block'
button3.style.marginLeft = 'auto'
@@ -385,7 +385,7 @@
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.tab-actions-right.sizing-shrink > .tab-actions,
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.tab-actions-right.sizing-fixed > .tab-actions {
flex: 0;
overflow: visible; /* ensure tab actions are always visible */
overflow: hidden; /* let the tab actions be pushed out of view when sizing is set to shrink/fixed to make more room */
}
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.dirty.tab-actions-right.sizing-shrink > .tab-actions,
@@ -399,8 +399,18 @@
overflow: visible; /* ...but still show the tab actions on hover, focus and when dirty or sticky */
}
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.close-action-off:not(.dirty) > .tab-actions,
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sticky-compact > .tab-actions {
display: none; /* only hide tab actions when sticky-compact */
display: none; /* hide the tab actions when we are configured to hide it (unless dirty, but always when sticky-compact) */
}
.monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab.active > .tab-actions .action-label, /* always show tab actions for active tab */
.monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab > .tab-actions .action-label:focus, /* always show tab actions on focus */
.monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab:hover > .tab-actions .action-label, /* always show tab actions on hover */
.monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab.active:hover > .tab-actions .action-label, /* always show tab actions on hover */
.monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab.sticky:not(.pinned-action-off) > .tab-actions .action-label, /* always show tab actions for sticky tabs */
.monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab.dirty > .tab-actions .action-label { /* always show tab actions for dirty tabs */
opacity: 1;
}
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab > .tab-actions .actions-container {
@@ -434,11 +444,11 @@
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.dirty > .tab-actions .action-label,
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sticky:not(.pinned-action-off) > .tab-actions .action-label,
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab:hover > .tab-actions .action-label {
opacity: 1;
opacity: 0.5; /* show tab actions dimmed for inactive group */
}
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab > .tab-actions .action-label {
opacity: 1;
opacity: 0;
}
/* Tab Actions: Off */
@@ -35,7 +35,7 @@ import { MergeGroupMode, IMergeGroupOptions } from '../../../services/editor/com
import { addDisposableListener, EventType, EventHelper, Dimension, scheduleAtNextAnimationFrame, findParentWithClass, clearNode, DragAndDropObserver, isMouseEvent, getWindow } from '../../../../base/browser/dom.js';
import { localize } from '../../../../nls.js';
import { IEditorGroupsView, EditorServiceImpl, IEditorGroupView, IInternalEditorOpenOptions, IEditorPartsView } from './editor.js';
import { CloseEditorTabAction, PinEditorAction, UnpinEditorAction } from './editorActions.js';
import { CloseEditorTabAction, UnpinEditorAction } from './editorActions.js';
import { assertAllDefined, assertIsDefined } from '../../../../base/common/types.js';
import { IEditorService } from '../../../services/editor/common/editorService.js';
import { basenameOrAuthority } from '../../../../base/common/resources.js';
@@ -113,7 +113,6 @@ export class MultiEditorTabsControl extends EditorTabsControl {
private readonly closeEditorAction = this._register(this.instantiationService.createInstance(CloseEditorTabAction, CloseEditorTabAction.ID, CloseEditorTabAction.LABEL));
private readonly unpinEditorAction = this._register(this.instantiationService.createInstance(UnpinEditorAction, UnpinEditorAction.ID, UnpinEditorAction.LABEL));
private readonly pinEditorAction = this._register(this.instantiationService.createInstance(PinEditorAction, PinEditorAction.ID, PinEditorAction.LABEL));
private readonly tabResourceLabels = this._register(this.instantiationService.createInstance(ResourceLabels, DEFAULT_LABELS_CONTAINER));
private tabLabels: IEditorInputLabel[] = [];
@@ -1511,8 +1510,6 @@ export class MultiEditorTabsControl extends EditorTabsControl {
this.layout(this.dimensions, options);
}
// In MultiEditorTabsControl.ts, modify the redrawTab method:
private redrawTab(editor: EditorInput, tabIndex: number, tabContainer: HTMLElement, tabLabelWidget: IResourceLabel, tabLabel: IEditorInputLabel, tabActionBar: ActionBar): void {
const isTabSticky = this.tabsModel.isSticky(tabIndex);
const options = this.groupsView.partOptions;
@@ -1521,46 +1518,47 @@ export class MultiEditorTabsControl extends EditorTabsControl {
this.redrawTabLabel(editor, tabIndex, tabContainer, tabLabelWidget, tabLabel);
// Action
const hasCloseAction = options.tabActionCloseVisibility;
const hasAction = true; // Always show actions
const hasUnpinAction = isTabSticky && options.tabActionUnpinVisibility;
const hasCloseAction = !hasUnpinAction && options.tabActionCloseVisibility;
const hasAction = hasUnpinAction || hasCloseAction;
// Clear existing actions
if (!tabActionBar.isEmpty()) {
tabActionBar.clear();
}
// Add pin/unpin action based on sticky state
if (isTabSticky) {
tabActionBar.push(this.unpinEditorAction, { icon: true, label: false, keybinding: this.getKeybindingLabel(this.unpinEditorAction) });
let tabAction;
if (hasAction) {
tabAction = hasUnpinAction ? this.unpinEditorAction : this.closeEditorAction;
} else {
tabActionBar.push(this.pinEditorAction, { icon: true, label: false, keybinding: this.getKeybindingLabel(this.pinEditorAction) });
// Even if the action is not visible, add it as it contains the dirty indicator
tabAction = isTabSticky ? this.unpinEditorAction : this.closeEditorAction;
}
// Add close action
if (hasCloseAction) {
tabActionBar.push(this.closeEditorAction, { icon: true, label: false, keybinding: this.getKeybindingLabel(this.closeEditorAction) });
if (!tabActionBar.hasAction(tabAction)) {
if (!tabActionBar.isEmpty()) {
tabActionBar.clear();
}
tabActionBar.push(tabAction, { icon: true, label: false, keybinding: this.getKeybindingLabel(tabAction) });
}
// Update tab classes and styles
tabContainer.classList.toggle('sticky', isTabSticky);
tabContainer.classList.toggle('close-action-off', !hasCloseAction);
tabContainer.classList.toggle('tab-actions-right', hasAction && options.tabActionLocation === 'right');
tabContainer.classList.toggle('tab-actions-left', hasAction && options.tabActionLocation === 'left');
tabContainer.classList.toggle(`pinned-action-off`, isTabSticky && !hasUnpinAction);
tabContainer.classList.toggle(`close-action-off`, !hasUnpinAction && !hasCloseAction);
// Update tab sizing classes
const tabSizing = isTabSticky && options.pinnedTabSizing === 'shrink' ? 'shrink' : options.tabSizing;
tabContainer.classList.toggle('sizing-fit', tabSizing === 'fit');
tabContainer.classList.toggle('sizing-shrink', tabSizing === 'shrink');
tabContainer.classList.toggle('sizing-fixed', tabSizing === 'fixed');
for (const option of ['left', 'right']) {
tabContainer.classList.toggle(`tab-actions-${option}`, hasAction && options.tabActionLocation === option);
}
const tabSizing = isTabSticky && options.pinnedTabSizing === 'shrink' ? 'shrink' /* treat sticky shrink tabs as tabSizing: 'shrink' */ : options.tabSizing;
for (const option of ['fit', 'shrink', 'fixed']) {
tabContainer.classList.toggle(`sizing-${option}`, tabSizing === option);
}
tabContainer.classList.toggle('has-icon', options.showIcons && options.hasIcons);
// Update sticky classes
tabContainer.classList.toggle('sticky', isTabSticky);
tabContainer.classList.toggle('sticky-compact', isTabSticky && options.pinnedTabSizing === 'compact');
tabContainer.classList.toggle('sticky-shrink', isTabSticky && options.pinnedTabSizing === 'shrink');
for (const option of ['normal', 'compact', 'shrink']) {
tabContainer.classList.toggle(`sticky-${option}`, isTabSticky && options.pinnedTabSizing === option);
}
// Update tab position if needed
// If not wrapping tabs, sticky compact/shrink tabs need a position to remain at their location
// when scrolling to stay in view (requirement for position: sticky)
if (!options.wrapTabs && isTabSticky && options.pinnedTabSizing !== 'normal') {
let stickyTabWidth = 0;
switch (options.pinnedTabSizing) {
@@ -1571,13 +1569,16 @@ export class MultiEditorTabsControl extends EditorTabsControl {
stickyTabWidth = MultiEditorTabsControl.TAB_WIDTH.shrink;
break;
}
tabContainer.style.left = `${tabIndex * stickyTabWidth}px`;
} else {
tabContainer.style.left = 'auto';
}
// Draw borders and selection state
// Borders / outline
this.redrawTabBorders(tabIndex, tabContainer);
// Selection / active / dirty state
this.redrawTabSelectedActiveAndDirty(this.groupsView.activeGroup === this.groupView, editor, tabContainer, tabActionBar);
}
@@ -253,7 +253,7 @@
}
.monaco-workbench .part.titlebar > .titlebar-container > .titlebar-left > .window-appicon:not(.codicon) {
background-image: url('../../../media/void-icon-sm.png');
background-image: url('../../../media/code-icon.svg');
background-repeat: no-repeat;
background-position: center center;
background-size: 16px;
@@ -275,7 +275,7 @@
height: 8px;
z-index: 1;
/* on top of home indicator */
background-image: url('../../../media/void-icon-sm.png');
background-image: url('../../../media/code-icon.svg');
background-repeat: no-repeat;
background-position: center center;
background-size: 8px;
@@ -289,7 +289,6 @@ export interface IFileTemplateData {
readonly templateDisposables: DisposableStore;
readonly elementDisposables: DisposableStore;
readonly label: IResourceLabel;
// readonly voidLabels: IResourceLabel;
readonly container: HTMLElement;
readonly contribs: IExplorerFileContribution[];
currentContext?: ExplorerItem;
@@ -348,25 +347,15 @@ export class FilesRenderer implements ICompressibleTreeRenderer<ExplorerItem, Fu
renderTemplate(container: HTMLElement): IFileTemplateData {
const templateDisposables = new DisposableStore();
// Void added this
// // Create void buttons container
// const voidButtonsContainer = DOM.append(container, DOM.$('div'));
// voidButtonsContainer.style.position = 'absolute'
// voidButtonsContainer.style.top = '0'
// voidButtonsContainer.style.right = '0'
// // const voidButtons = DOM.append(voidButtonsContainer, DOM.$('span'));
// // voidButtons.textContent = 'voidbuttons'
// // voidButtons.addEventListener('click', () => {
// // console.log('ON CLICK', templateData.currentContext?.children)
// // })
// const voidLabels = this.labels.create(voidButtonsContainer, { supportHighlights: false, supportIcons: false, });
// voidLabels.element.textContent = 'hi333'
const label = templateDisposables.add(this.labels.create(container, { supportHighlights: true }));
templateDisposables.add(label.onDidRender(() => {
try { if (templateData.currentContext) this.updateWidth(templateData.currentContext); }
catch (e) { /* noop since the element might no longer be in the tree, no update of width necessary*/ }
try {
if (templateData.currentContext) {
this.updateWidth(templateData.currentContext);
}
} catch (e) {
// noop since the element might no longer be in the tree, no update of width necessary
}
}));
const contribs = explorerFileContribRegistry.create(this.instantiationService, container, templateDisposables);
@@ -376,15 +365,10 @@ export class FilesRenderer implements ICompressibleTreeRenderer<ExplorerItem, Fu
contr.setResource(templateData.currentContext?.resource);
}));
// const templateData: IFileTemplateData = { templateDisposables, elementDisposables: templateDisposables.add(new DisposableStore()), label, voidLabels, container, contribs };
const templateData: IFileTemplateData = { templateDisposables, elementDisposables: templateDisposables.add(new DisposableStore()), label, container, contribs };
return templateData;
}
// Void cares about this function, this is where elements in the tree are rendered
renderElement(node: ITreeNode<ExplorerItem, FuzzyScore>, index: number, templateData: IFileTemplateData): void {
const stat = node.element;
templateData.currentContext = stat;
@@ -398,7 +382,8 @@ export class FilesRenderer implements ICompressibleTreeRenderer<ExplorerItem, Fu
templateData.label.element.style.display = 'flex';
this.renderStat(stat, stat.name, undefined, node.filterData, templateData);
}
// Input Box (Void - shown only if currently editing - this is the box that appears when user edits the name of the file)
// Input Box
else {
templateData.label.element.style.display = 'none';
templateData.contribs.forEach(c => c.setResource(undefined));
@@ -492,13 +477,6 @@ export class FilesRenderer implements ICompressibleTreeRenderer<ExplorerItem, Fu
separator: this.labelService.getSeparator(stat.resource.scheme, stat.resource.authority),
domId
});
// templateData.voidLabels.setResource({ resource: undefined, name: 'hi', }, {
// hideIcon: true,
// extraClasses: realignNestedChildren ? [...extraClasses, 'align-nest-icon-with-parent-icon'] : extraClasses,
// forceLabel: true,
// });
}
private renderInputBox(container: HTMLElement, stat: ExplorerItem, editableData: IEditableData): IDisposable {
@@ -95,12 +95,7 @@ suite('Files - ExplorerView', () => {
label: <any>{
container: label,
onDidRender: emitter.event
},
// voidLabels: <any>{
// container: label,
// onDidRender: emitter.event
// },
}
}, 1, false);
ds.add(navigationController);
@@ -5,5 +5,5 @@
.file-icons-enabled .show-file-icons .webview-vs_code_release_notes-name-file-icon.file-icon::before {
content: ' ';
background-image: url('../../../../browser/media/void-icon-sm.png');
background-image: url('../../../../browser/media/code-icon.svg');
}
@@ -1,6 +0,0 @@
// Normally you'd want to put these exports in the files that register them, but if you do that you'll get an import order error if you import them in certain cases.
// (importing them runs the whole file to get the ID, causing an import error). I guess it's best practice to separate out IDs, pretty annoying...
export const VOID_CTRL_L_ACTION_ID = 'void.ctrlLAction'
export const VOID_CTRL_K_ACTION_ID = 'void.ctrlKAction'
@@ -1,25 +1,23 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
*-----------------------------------------------------------------------------------------*/
import { Disposable } from '../../../../base/common/lifecycle.js';
import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js';
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { ITextModel } from '../../../../editor/common/model.js';
import { Position } from '../../../../editor/common/core/position.js';
import { InlineCompletion, InlineCompletionContext, } from '../../../../editor/common/languages.js';
import { InlineCompletion, InlineCompletionContext } from '../../../../editor/common/languages.js';
import { CancellationToken } from '../../../../base/common/cancellation.js';
import { Range } from '../../../../editor/common/core/range.js';
import { ILLMMessageService } from '../../../../platform/void/common/llmMessageService.js';
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 { extractCodeFromRegular } from './helpers/extractCodeFromResult.js';
import { isWindows } from '../../../../base/common/platform.js';
import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js';
import { ILLMMessageService } from '../common/llmMessageService.js';
// import { IContextGatheringService } from './contextGatheringService.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
@@ -136,27 +134,17 @@ class LRUCache<K, V> {
}
}
type AutocompletionPredictionType =
| 'single-line-fill-middle'
| 'single-line-redo-suffix'
// | 'multi-line-start-here'
| 'multi-line-start-on-next-line'
| 'do-not-predict'
type AutocompletionStatus = 'pending' | 'finished' | 'error';
type Autocompletion = {
id: number,
prefix: string,
suffix: string,
llmPrefix: string,
llmSuffix: string,
startTime: number,
endTime: number | undefined,
status: 'pending' | 'finished' | 'error',
type: AutocompletionPredictionType,
status: AutocompletionStatus,
llmPromise: Promise<string> | undefined,
insertText: string,
requestId: string | null,
_newlineCount: number,
}
const DEBOUNCE_TIME = 500
@@ -165,16 +153,13 @@ const MAX_CACHE_SIZE = 20
const MAX_PENDING_REQUESTS = 2
// postprocesses the result
const processStartAndEndSpaces = (result: string) => {
const postprocessResult = (result: string) => {
// trim all whitespace except for a single leading/trailing space
// return result.trim()
[result,] = extractCodeFromRegular({ text: result, recentlyAddedTextLen: result.length })
const hasLeadingSpace = result.startsWith(' ');
const hasTrailingSpace = result.endsWith(' ');
return (hasLeadingSpace ? ' ' : '')
+ result.trim()
+ (hasTrailingSpace ? ' ' : '');
@@ -183,13 +168,13 @@ const processStartAndEndSpaces = (result: string) => {
// trims the end of the prefix to improve cache hit rate
const removeLeftTabsAndTrimEnds = (s: string): string => {
const removeLeftTabsAndTrimEnd = (s: string): string => {
const trimmedString = s.trimEnd();
const trailingEnd = s.slice(trimmedString.length);
// keep only a single trailing newline
if (trailingEnd.includes(_ln)) {
s = trimmedString + _ln;
if (trailingEnd.includes('\n')) {
s = trimmedString + '\n';
}
s = s.replace(/^\s+/gm, ''); // remove left tabs
@@ -199,32 +184,7 @@ const removeLeftTabsAndTrimEnds = (s: string): string => {
const removeAllWhitespace = (str: string): string => str.replace(/\s+/g, '');
function getIsSubsequence({ of, subsequence }: { of: string, subsequence: string }): [boolean, string] {
if (subsequence.length === 0) return [true, ''];
if (of.length === 0) return [false, ''];
let subsequenceIndex = 0;
let lastMatchChar = '';
for (let i = 0; i < of.length; i++) {
if (of[i] === subsequence[subsequenceIndex]) {
lastMatchChar = of[i];
subsequenceIndex++;
}
if (subsequenceIndex === subsequence.length) {
return [true, lastMatchChar];
}
}
return [false, lastMatchChar];
}
function getStringUpToUnbalancedClosingParenthesis(s: string, prefix: string): string {
function getStringUpToUnbalancedParenthesis(s: string, prefix: string): string {
const pairs: Record<string, string> = { ')': '(', '}': '{', ']': '[' };
@@ -260,14 +220,19 @@ function getStringUpToUnbalancedClosingParenthesis(s: string, prefix: string): s
}
// further trim the autocompletion
const postprocessAutocompletion = ({ autocompletionMatchup, autocompletion, prefixAndSuffix }: { autocompletionMatchup: AutocompletionMatchupBounds, autocompletion: Autocompletion, prefixAndSuffix: PrefixAndSuffixInfo }) => {
const parenthesisChars = `{}()[]<>\`'"`
const { prefix, prefixToTheLeftOfCursor, suffixToTheRightOfCursor } = prefixAndSuffix
// returns the text in the autocompletion to display, assuming the prefix is already matched
const toInlineCompletions = ({ matchInfo, prefix, suffix, autocompletion, position, debug }: { matchInfo: matchInfo, prefix: string, suffix: string, autocompletion: Autocompletion, position: Position, debug?: boolean }): { insertText: string, range: Range }[] => {
const suffixLines = suffix.split('\n')
const prefixLines = prefix.split('\n')
const suffixToTheRightOfCursor = suffixLines[0]
const prefixToTheLeftOfCursor = prefixLines[prefixLines.length - 1]
const generatedMiddle = autocompletion.insertText
let startIdx = autocompletionMatchup.startIdx
let startIdx = matchInfo.startIdx
let endIdx = generatedMiddle.length // exclusive bounds
// const naiveReturnValue = generatedMiddle.slice(startIdx)
@@ -288,7 +253,7 @@ const postprocessAutocompletion = ({ autocompletionMatchup, autocompletion, pref
}
// if user is on a blank line and the generation starts with newline(s), remove them
const numStartingNewlines = generatedMiddle.slice(startIdx).match(new RegExp(`^${_ln}+`))?.[0].length || 0;
const numStartingNewlines = generatedMiddle.slice(startIdx).match(/^\n+/)?.[0].length || 0;
if (
!prefixToTheLeftOfCursor.trim()
&& !suffixToTheRightOfCursor.trim()
@@ -298,21 +263,21 @@ const postprocessAutocompletion = ({ autocompletionMatchup, autocompletion, pref
startIdx += numStartingNewlines
}
// if the generated FIM text matches with the suffix on the current line, stop
if (autocompletion.type === 'single-line-fill-middle' && suffixToTheRightOfCursor.trim()) { // completing in the middle of a line
// if the generated text matches with the suffix on the current line, stop
if (suffixToTheRightOfCursor.trim()) { // completing in the middle of a line
// complete until there is a match
const rawMatchIndex = generatedMiddle.slice(startIdx).lastIndexOf(suffixToTheRightOfCursor.trim()[0])
if (rawMatchIndex > -1) {
// console.log('p2', rawMatchIndex, startIdx, suffixToTheRightOfCursor.trim()[0], 'AAA', generatedMiddle.slice(startIdx))
const matchIdx = rawMatchIndex + startIdx;
const matchChar = generatedMiddle[matchIdx]
if (`{}()[]<>\`'"`.includes(matchChar)) {
if (parenthesisChars.includes(matchChar)) {
endIdx = Math.min(endIdx, matchIdx)
}
}
}
const restOfLineToGenerate = generatedMiddle.slice(startIdx).split(_ln)[0] ?? ''
const restOfLineToGenerate = generatedMiddle.slice(startIdx).split('\n')[0] ?? ''
// condition to complete as a single line completion
if (
prefixToTheLeftOfCursor.trim()
@@ -320,7 +285,7 @@ const postprocessAutocompletion = ({ autocompletionMatchup, autocompletion, pref
&& restOfLineToGenerate.trim()
) {
const rawNewlineIdx = generatedMiddle.slice(startIdx).indexOf(_ln)
const rawNewlineIdx = generatedMiddle.slice(startIdx).indexOf('\n')
if (rawNewlineIdx > -1) {
// console.log('p3', startIdx, rawNewlineIdx)
const newlineIdx = rawNewlineIdx + startIdx;
@@ -346,48 +311,14 @@ const postprocessAutocompletion = ({ autocompletionMatchup, autocompletion, pref
let completionStr = generatedMiddle.slice(startIdx, endIdx)
// filter out unbalanced parentheses
completionStr = getStringUpToUnbalancedClosingParenthesis(completionStr, prefix)
completionStr = getStringUpToUnbalancedParenthesis(completionStr, prefix)
// console.log('originalCompletionStr: ', JSON.stringify(generatedMiddle.slice(startIdx)))
// console.log('finalCompletionStr: ', JSON.stringify(completionStr))
return completionStr
}
// returns the text in the autocompletion to display, assuming the prefix is already matched
const toInlineCompletions = ({ autocompletionMatchup, autocompletion, prefixAndSuffix, position, debug }: { autocompletionMatchup: AutocompletionMatchupBounds, autocompletion: Autocompletion, prefixAndSuffix: PrefixAndSuffixInfo, position: Position, debug?: boolean }): { insertText: string, range: Range }[] => {
let trimmedInsertText = postprocessAutocompletion({ autocompletionMatchup, autocompletion, prefixAndSuffix, })
let rangeToReplace: Range = new Range(position.lineNumber, position.column, position.lineNumber, position.column)
// handle special cases
// if we redid the suffix, replace the suffix
if (autocompletion.type === 'single-line-redo-suffix') {
const oldSuffix = prefixAndSuffix.suffixToTheRightOfCursor
const newSuffix = autocompletion.insertText
const [isSubsequence, lastMatchingChar] = getIsSubsequence({ // check that the old text contains the same brackets + symbols as the new text
subsequence: removeAllWhitespace(oldSuffix), // old suffix
of: removeAllWhitespace(newSuffix), // new suffix
})
if (isSubsequence) {
rangeToReplace = new Range(position.lineNumber, position.column, position.lineNumber, Number.MAX_SAFE_INTEGER)
}
else {
const lastMatchupIdx = trimmedInsertText.lastIndexOf(lastMatchingChar)
trimmedInsertText = trimmedInsertText.slice(0, lastMatchupIdx + 1)
const numCharsToReplace = oldSuffix.lastIndexOf(lastMatchingChar) + 1
rangeToReplace = new Range(position.lineNumber, position.column, position.lineNumber, position.column + numCharsToReplace)
// console.log('show____', trimmedInsertText, rangeToReplace)
}
}
return [{
insertText: trimmedInsertText,
insertText: completionStr,
range: rangeToReplace,
}]
@@ -414,12 +345,7 @@ const toInlineCompletions = ({ autocompletionMatchup, autocompletion, prefixAndS
// }
const allLinebreakSymbols = ['\r\n', '\n']
const _ln = isWindows ? allLinebreakSymbols[0] : allLinebreakSymbols[1]
type PrefixAndSuffixInfo = { prefix: string, suffix: string, prefixLines: string[], suffixLines: string[], prefixToTheLeftOfCursor: string, suffixToTheRightOfCursor: string }
const getPrefixAndSuffixInfo = (model: ITextModel, position: Position): PrefixAndSuffixInfo => {
const getPrefixAndSuffix = (model: ITextModel, position: Position) => {
const fullText = model.getValue();
@@ -427,37 +353,30 @@ const getPrefixAndSuffixInfo = (model: ITextModel, position: Position): PrefixAn
const prefix = fullText.substring(0, cursorOffset)
const suffix = fullText.substring(cursorOffset)
const prefixLines = prefix.split(_ln)
const suffixLines = suffix.split(_ln)
const prefixToTheLeftOfCursor = prefixLines.slice(-1)[0] ?? ''
const suffixToTheRightOfCursor = suffixLines[0] ?? ''
return { prefix, suffix, prefixLines, suffixLines, prefixToTheLeftOfCursor, suffixToTheRightOfCursor }
return { prefix, suffix }
}
const getIndex = (str: string, line: number, char: number) => {
return str.split(_ln).slice(0, line).join(_ln).length + (line > 0 ? 1 : 0) + char;
return str.split('\n').slice(0, line).join('\n').length + (line > 0 ? 1 : 0) + char;
}
const getLastLine = (s: string): string => {
const matches = s.match(new RegExp(`[^${_ln}]*$`))
const matches = s.match(/[^\n]*$/)
return matches ? matches[0] : ''
}
type AutocompletionMatchupBounds = {
startLine: number,
startCharacter: number,
type matchInfo = {
lineStart: number,
character: number,
startIdx: number,
}
// returns the startIdx of the match if there is a match, or undefined if there is no match
// all results are wrt `autocompletion.result`
const getAutocompletionMatchup = ({ prefix, autocompletion }: { prefix: string, autocompletion: Autocompletion }): AutocompletionMatchupBounds | undefined => {
const getPrefixAutocompletionMatch = ({ prefix, autocompletion }: { prefix: string, autocompletion: Autocompletion }): matchInfo | undefined => {
const trimmedCurrentPrefix = removeLeftTabsAndTrimEnds(prefix)
const trimmedCompletionPrefix = removeLeftTabsAndTrimEnds(autocompletion.prefix)
const trimmedCompletionMiddle = removeLeftTabsAndTrimEnds(autocompletion.insertText)
const trimmedCurrentPrefix = removeLeftTabsAndTrimEnd(prefix)
const trimmedCompletionPrefix = removeLeftTabsAndTrimEnd(autocompletion.prefix)
const trimmedCompletionMiddle = removeLeftTabsAndTrimEnd(autocompletion.insertText)
// console.log('@result: ', JSON.stringify(autocompletion.insertText))
// console.log('@trimmedCurrentPrefix: ', JSON.stringify(trimmedCurrentPrefix))
@@ -465,7 +384,7 @@ const getAutocompletionMatchup = ({ prefix, autocompletion }: { prefix: string,
// console.log('@trimmedCompletionMiddle: ', JSON.stringify(trimmedCompletionMiddle))
if (trimmedCurrentPrefix.length < trimmedCompletionPrefix.length) { // user must write text beyond the original prefix at generation time
// console.log('@undefined1')
console.log('@undefined1')
return undefined
}
@@ -473,24 +392,24 @@ const getAutocompletionMatchup = ({ prefix, autocompletion }: { prefix: string,
!(trimmedCompletionPrefix + trimmedCompletionMiddle)
.startsWith(trimmedCurrentPrefix)
) {
// console.log('@undefined2')
console.log('@undefined2')
return undefined
}
// reverse map to find position wrt `autocompletion.result`
const lineStart =
trimmedCurrentPrefix.split(_ln).length -
trimmedCompletionPrefix.split(_ln).length;
trimmedCurrentPrefix.split('\n').length -
trimmedCompletionPrefix.split('\n').length;
if (lineStart < 0) {
// console.log('@undefined3')
console.log('@undefined3')
console.error('Error: No line found.');
return undefined;
}
const currentPrefixLine = getLastLine(trimmedCurrentPrefix)
const completionPrefixLine = lineStart === 0 ? getLastLine(trimmedCompletionPrefix) : ''
const completionMiddleLine = autocompletion.insertText.split(_ln)[lineStart]
const completionMiddleLine = autocompletion.insertText.split('\n')[lineStart]
const fullCompletionLine = completionPrefixLine + completionMiddleLine
// console.log('currentPrefixLine', currentPrefixLine)
@@ -499,7 +418,7 @@ const getAutocompletionMatchup = ({ prefix, autocompletion }: { prefix: string,
const charMatchIdx = fullCompletionLine.indexOf(currentPrefixLine)
if (charMatchIdx < 0) {
// console.log('@undefined4', charMatchIdx)
console.log('@undefined4', charMatchIdx)
console.error('Warning: Found character with negative index. This should never happen.')
return undefined
@@ -513,8 +432,8 @@ const getAutocompletionMatchup = ({ prefix, autocompletion }: { prefix: string,
const startIdx = getIndex(autocompletion.insertText, lineStart, character)
return {
startLine: lineStart,
startCharacter: character,
lineStart,
character,
startIdx,
}
@@ -522,89 +441,40 @@ const getAutocompletionMatchup = ({ prefix, autocompletion }: { prefix: string,
}
type CompletionOptions = {
predictionType: AutocompletionPredictionType,
shouldGenerate: boolean,
llmPrefix: string,
llmSuffix: string,
stopTokens: string[],
}
const getCompletionOptions = (prefixAndSuffix: PrefixAndSuffixInfo, relevantContext: string, justAcceptedAutocompletion: boolean): CompletionOptions => {
let { prefix, suffix, prefixToTheLeftOfCursor, suffixToTheRightOfCursor, suffixLines, prefixLines } = prefixAndSuffix
// trim prefix and suffix to not be very large
suffixLines = suffix.split(_ln).slice(0, 25)
prefixLines = prefix.split(_ln).slice(-25)
prefix = prefixLines.join(_ln)
suffix = suffixLines.join(_ln)
const getCompletionOptions = ({ prefix, suffix }: { prefix: string, suffix: string }) => {
let completionOptions: CompletionOptions
const prefixLines = prefix.split('\n')
const suffixLines = suffix.split('\n')
// if line is empty, do multiline completion
const isLineEmpty = !prefixToTheLeftOfCursor.trim() && !suffixToTheRightOfCursor.trim()
const isLinePrefixEmpty = removeAllWhitespace(prefixToTheLeftOfCursor).length === 0
const isLineSuffixEmpty = removeAllWhitespace(suffixToTheRightOfCursor).length === 0
const prefixToLeftOfCursor = prefixLines.slice(-1)[0] ?? ''
const suffixToRightOfCursor = suffixLines[0] ?? ''
// TODO add context to prefix
// llmPrefix = '\n\n/* Relevant context:\n' + relevantContext + '\n*/\n' + llmPrefix
// default parameters
let shouldGenerate = true
let stopTokens: string[] = ['\n\n', '\r\n\r\n']
// if we just accepted an autocompletion, predict a multiline completion starting on the next line
if (justAcceptedAutocompletion && isLineSuffixEmpty) {
const prefixWithNewline = prefix + _ln
completionOptions = {
predictionType: 'multi-line-start-on-next-line',
shouldGenerate: true,
llmPrefix: prefixWithNewline,
llmSuffix: suffix,
stopTokens: [`${_ln}${_ln}`] // double newlines
}
}
// if the current line is empty, predict a single-line completion
else if (isLineEmpty) {
completionOptions = {
predictionType: 'single-line-fill-middle',
shouldGenerate: true,
llmPrefix: prefix,
llmSuffix: suffix,
stopTokens: allLinebreakSymbols
}
}
// if suffix is 3 or fewer characters, attempt to complete the line ignorning it
else if (removeAllWhitespace(suffixToTheRightOfCursor).length <= 3) {
const suffixLinesIgnoringThisLine = suffixLines.slice(1)
const suffixStringIgnoringThisLine = suffixLinesIgnoringThisLine.length === 0 ? '' : _ln + suffixLinesIgnoringThisLine.join(_ln)
completionOptions = {
predictionType: 'single-line-redo-suffix',
shouldGenerate: true,
llmPrefix: prefix,
llmSuffix: suffixStringIgnoringThisLine,
stopTokens: allLinebreakSymbols
}
}
// else attempt to complete the middle of the line if there is a prefix (the completion looks bad if there is no prefix)
else if (!isLinePrefixEmpty) {
completionOptions = {
predictionType: 'single-line-fill-middle',
shouldGenerate: true,
llmPrefix: prefix,
llmSuffix: suffix,
stopTokens: allLinebreakSymbols
}
} else {
completionOptions = {
predictionType: 'do-not-predict',
shouldGenerate: false,
llmPrefix: prefix,
llmSuffix: suffix,
stopTokens: []
}
// specific cases
if (suffixToRightOfCursor.trim() !== '') { // typing between something
stopTokens = ['\n', '\r\n']
}
return completionOptions
// if (prefixToLeftOfCursor.trim() === '' && suffixToRightOfCursor.trim() === '') { // at an empty line
// stopTokens = ['\n\n', '\r\n\r\n']
// }
if (prefixToLeftOfCursor === '') { // at beginning or end of line
shouldGenerate = false
}
return { shouldGenerate, stopTokens }
}
export interface IAutocompleteService {
readonly _serviceBrand: undefined;
}
@@ -612,17 +482,13 @@ export interface IAutocompleteService {
export const IAutocompleteService = createDecorator<IAutocompleteService>('AutocompleteService');
export class AutocompleteService extends Disposable implements IAutocompleteService {
static readonly ID = 'void.autocompleteService'
_serviceBrand: undefined;
private _autocompletionId: number = 0;
private _autocompletionsOfDocument: { [docUriStr: string]: LRUCache<number, Autocompletion> } = {}
private _lastCompletionStart = 0
private _lastCompletionAccept = 0
// private _lastPrefix: string = ''
private _lastCompletionTime = 0
private _lastPrefix: string = ''
// used internally by vscode
// fires after every keystroke and returns the completion to show
@@ -633,15 +499,16 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
token: CancellationToken,
): Promise<InlineCompletion[]> {
const disabled = true
const testMode = false
if (disabled) return [];
const docUriStr = model.uri.toString();
const prefixAndSuffix = getPrefixAndSuffixInfo(model, position)
const { prefix, suffix } = prefixAndSuffix
// initialize cache if it doesnt exist
// note that whenever an autocompletion is accepted, it is removed from cache
const { prefix, suffix } = getPrefixAndSuffix(model, position)
// initialize cache and other variables
// note that whenever an autocompletion is rejected, it is removed from cache
if (!this._autocompletionsOfDocument[docUriStr]) {
this._autocompletionsOfDocument[docUriStr] = new LRUCache<number, Autocompletion>(
MAX_CACHE_SIZE,
@@ -651,7 +518,7 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
}
)
}
// this._lastPrefix = prefix
this._lastPrefix = prefix
// print all pending autocompletions
// let _numPending = 0
@@ -660,36 +527,33 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
// get autocompletion from cache
let cachedAutocompletion: Autocompletion | undefined = undefined
let autocompletionMatchup: AutocompletionMatchupBounds | undefined = undefined
let matchInfo: matchInfo | undefined = undefined
for (const autocompletion of this._autocompletionsOfDocument[docUriStr].items.values()) {
// if the user's change matches with the autocompletion
autocompletionMatchup = getAutocompletionMatchup({ prefix, autocompletion })
if (autocompletionMatchup !== undefined) {
// if the user's change matches up with the generated text
matchInfo = getPrefixAutocompletionMatch({ prefix, autocompletion })
if (matchInfo !== undefined) {
cachedAutocompletion = autocompletion
break;
}
}
// if there is a cached autocompletion, return it
if (cachedAutocompletion && autocompletionMatchup) {
console.log('AA')
if (cachedAutocompletion && matchInfo) {
// console.log('id: ' + cachedAutocompletion.id)
if (cachedAutocompletion.status === 'finished') {
console.log('A1')
// console.log('A1')
const inlineCompletions = toInlineCompletions({ autocompletionMatchup, autocompletion: cachedAutocompletion, prefixAndSuffix, position, debug: true })
const inlineCompletions = toInlineCompletions({ matchInfo, autocompletion: cachedAutocompletion, prefix, suffix, position, debug: true })
return inlineCompletions
} else if (cachedAutocompletion.status === 'pending') {
console.log('A2')
// console.log('A2')
try {
await cachedAutocompletion.llmPromise;
const inlineCompletions = toInlineCompletions({ autocompletionMatchup, autocompletion: cachedAutocompletion, prefixAndSuffix, position })
const inlineCompletions = toInlineCompletions({ matchInfo, autocompletion: cachedAutocompletion, prefix, suffix, position })
return inlineCompletions
} catch (e) {
@@ -698,25 +562,19 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
}
} else if (cachedAutocompletion.status === 'error') {
console.log('A3')
} else {
console.log('A4')
// console.log('A3')
}
return []
}
// else if no more typing happens, then go forwards with the request
// wait DEBOUNCE_TIME for the user to stop typing
const thisTime = Date.now()
const justAcceptedAutocompletion = thisTime - this._lastCompletionAccept < 500
this._lastCompletionStart = thisTime
this._lastCompletionTime = thisTime
const didTypingHappenDuringDebounce = await new Promise((resolve, reject) =>
setTimeout(() => {
if (this._lastCompletionStart === thisTime) {
if (this._lastCompletionTime === thisTime) {
resolve(false)
} else {
resolve(true)
@@ -747,15 +605,7 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
}
}
// gather relevant context from the code around the user's selection and definitions
// const relevantSnippetsList = await this._contextGatheringService.readCachedSnippets(model, position, 3);
// const relevantSnippetsList = this._contextGatheringService.getCachedSnippets();
// const relevantSnippets = relevantSnippetsList.map((text) => `${text}`).join('\n-------------------------------\n')
// console.log('@@---------------------\n' + relevantSnippets)
const relevantContext = ''
const { shouldGenerate, predictionType, llmPrefix, llmSuffix, stopTokens } = getCompletionOptions(prefixAndSuffix, relevantContext, justAcceptedAutocompletion)
const { shouldGenerate, stopTokens: _ } = getCompletionOptions({ prefix, suffix }) // TODO mat
if (!shouldGenerate) return []
@@ -763,74 +613,46 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
return []
}
// console.log('B')
// create a new autocompletion and add it to cache
const newAutocompletion: Autocompletion = {
id: this._autocompletionId++,
prefix: prefix, // the actual prefix and suffix
prefix: prefix,
suffix: suffix,
llmPrefix: llmPrefix, // the prefix and suffix the llm sees
llmSuffix: llmSuffix,
startTime: Date.now(),
endTime: undefined,
type: predictionType,
status: 'pending',
llmPromise: undefined,
insertText: '',
requestId: null,
_newlineCount: 0,
}
console.log('starting autocomplete...', predictionType)
// set parameters of `newAutocompletion` appropriately
newAutocompletion.llmPromise = new Promise((resolve, reject) => {
const requestId = this._llmMessageService.sendLLMMessage({
messagesType: 'FIMMessage',
messages: {
prefix: llmPrefix,
suffix: llmSuffix,
stopTokens: stopTokens,
},
useProviderFor: 'Autocomplete',
logging: { loggingName: 'Autocomplete' },
onText: async ({ fullText, newText }) => {
messages: [],
onText: async ({ newText, fullText }) => {
newAutocompletion.insertText = fullText
// count newlines in newText
const numNewlines = newText.match(/\n|\r\n/g)?.length || 0
newAutocompletion._newlineCount += numNewlines
// if too many newlines, resolve up to last newline
if (newAutocompletion._newlineCount > 10) {
const lastNewlinePos = fullText.lastIndexOf('\n')
newAutocompletion.insertText = fullText.substring(0, lastNewlinePos)
resolve(newAutocompletion.insertText)
return
// if generation doesn't match the prefix for the first few tokens generated, reject it
if (!getPrefixAutocompletionMatch({ prefix: this._lastPrefix, autocompletion: newAutocompletion })) {
reject('LLM response did not match user\'s text.')
}
// if (!getAutocompletionMatchup({ prefix: this._lastPrefix, autocompletion: newAutocompletion })) {
// reject('LLM response did not match user\'s text.')
// }
},
onFinalMessage: ({ fullText }) => {
// console.log('____res: ', JSON.stringify(newAutocompletion.insertText))
// newAutocompletion.prefix = prefix
// newAutocompletion.suffix = suffix
// newAutocompletion.startTime = Date.now()
newAutocompletion.endTime = Date.now()
// newAutocompletion.abortRef = { current: () => { } }
newAutocompletion.status = 'finished'
const [text, _] = extractCodeFromRegular({ text: fullText, recentlyAddedTextLen: 0 })
newAutocompletion.insertText = processStartAndEndSpaces(text)
// handle special case for predicting starting on the next line, add a newline character
if (newAutocompletion.type === 'multi-line-start-on-next-line') {
newAutocompletion.insertText = _ln + newAutocompletion.insertText
}
// newAutocompletion.promise = undefined
newAutocompletion.insertText = postprocessResult(extractCodeFromResult(fullText))
resolve(newAutocompletion.insertText)
@@ -840,6 +662,8 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
newAutocompletion.status = 'error'
reject(message)
},
featureName: 'Autocomplete',
range: { startLineNumber: position.lineNumber, startColumn: position.column, endLineNumber: position.lineNumber, endColumn: position.column },
})
newAutocompletion.requestId = requestId
@@ -862,8 +686,8 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
await newAutocompletion.llmPromise
// console.log('id: ' + newAutocompletion.id)
const autocompletionMatchup: AutocompletionMatchupBounds = { startIdx: 0, startLine: 0, startCharacter: 0 }
const inlineCompletions = toInlineCompletions({ autocompletionMatchup, autocompletion: newAutocompletion, prefixAndSuffix, position })
const matchInfo: matchInfo = { startIdx: 0, lineStart: 0, character: 0 }
const inlineCompletions = toInlineCompletions({ matchInfo, autocompletion: newAutocompletion, prefix, suffix, position })
return inlineCompletions
} catch (e) {
@@ -879,7 +703,6 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
@ILLMMessageService private readonly _llmMessageService: ILLMMessageService,
@IEditorService private readonly _editorService: IEditorService,
@IModelService private readonly _modelService: IModelService,
// @IContextGatheringService private readonly _contextGatheringService: IContextGatheringService,
) {
super()
@@ -891,6 +714,7 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
return { items: items, }
},
freeInlineCompletions: (completions) => {
// get the `docUriStr` and the `position` of the cursor
const activePane = this._editorService.activeEditorPane;
if (!activePane) return;
@@ -903,32 +727,33 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
const model = this._modelService.getModel(resource)
if (!model) return;
const docUriStr = resource.toString();
if (!this._autocompletionsOfDocument[docUriStr]) return;
const { prefix, } = getPrefixAndSuffixInfo(model, position)
const { prefix, } = getPrefixAndSuffix(model, position)
if (!this._autocompletionsOfDocument[docUriStr]) return;
// go through cached items and remove matching ones
// autocompletion.prefix + autocompletion.insertedText ~== insertedText
this._autocompletionsOfDocument[docUriStr].items.forEach((autocompletion: Autocompletion) => {
// we can do this more efficiently, I just didn't want to deal with all of the edge cases
const matchup = removeAllWhitespace(prefix) === removeAllWhitespace(autocompletion.prefix + autocompletion.insertText)
if (matchup) {
console.log('ACCEPT', autocompletion.id)
this._lastCompletionAccept = Date.now()
this._autocompletionsOfDocument[docUriStr].delete(autocompletion.id);
}
completions.items.forEach(item => {
this._autocompletionsOfDocument[docUriStr].items.forEach((autocompletion: Autocompletion) => {
if (removeLeftTabsAndTrimEnd(prefix)
=== removeLeftTabsAndTrimEnd(autocompletion.prefix + autocompletion.insertText)
) {
this._autocompletionsOfDocument[docUriStr].delete(autocompletion.id);
}
});
});
},
})
}
}
registerWorkbenchContribution2(AutocompleteService.ID, AutocompleteService, WorkbenchPhase.BlockRestore);
registerSingleton(IAutocompleteService, AutocompleteService, InstantiationType.Eager);
@@ -1,552 +0,0 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { Disposable } from '../../../../base/common/lifecycle.js';
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
import { URI } from '../../../../base/common/uri.js';
import { Emitter, Event } from '../../../../base/common/event.js';
import { IRange } from '../../../../editor/common/core/range.js';
import { ILLMMessageService } from '../common/llmMessageService.js';
import { IModelService } from '../../../../editor/common/services/model.js';
import { chat_userMessageContent, chat_systemMessage, chat_userMessageContentWithAllFilesToo as chat_userMessageContentWithAllFiles } from './prompt/prompts.js';
import { LLMChatMessage } from '../common/llmMessageTypes.js';
import { IFileService } from '../../../../platform/files/common/files.js';
// one of the square items that indicates a selection in a chat bubble (NOT a file, a Selection of text)
export type CodeSelection = {
type: 'Selection';
fileURI: URI;
selectionStr: string;
range: IRange;
}
export type FileSelection = {
type: 'File';
fileURI: URI;
selectionStr: null;
range: null;
}
export type StagingSelectionItem = CodeSelection | FileSelection
// WARNING: changing this format is a big deal!!!!!! need to migrate old format to new format on users' computers so people don't get errors.
export type ChatMessage =
| {
role: 'user';
content: string | null; // content displayed to the LLM on future calls - allowed to be '', will be replaced with (empty)
displayContent: string | null; // content displayed to user - allowed to be '', will be ignored
selections: StagingSelectionItem[] | null; // the user's selection
state: {
stagingSelections: StagingSelectionItem[];
isBeingEdited: boolean;
}
}
| {
role: 'assistant';
content: string | null; // content received from LLM - allowed to be '', will be replaced with (empty)
displayContent: string | null; // content displayed to user (this is the same as content for now) - allowed to be '', will be ignored
}
| {
role: 'system';
content: string;
displayContent?: undefined;
}
type UserMessageType = ChatMessage & { role: 'user' }
type UserMessageState = UserMessageType['state']
export const defaultMessageState: UserMessageState = { stagingSelections: [], isBeingEdited: false }
// a 'thread' means a chat message history
export type ChatThreads = {
[id: string]: {
id: string; // store the id here too
createdAt: string; // ISO string
lastModified: string; // ISO string
messages: ChatMessage[];
state: {
stagingSelections: StagingSelectionItem[];
focusedMessageIdx: number | undefined; // index of the message that is being edited (undefined if none)
isCheckedOfSelectionId: { [selectionId: string]: boolean };
}
};
}
type ThreadType = ChatThreads[string]
const defaultThreadState: ThreadType['state'] = { stagingSelections: [], focusedMessageIdx: undefined, isCheckedOfSelectionId: {} }
export type ThreadsState = {
allThreads: ChatThreads;
currentThreadId: string; // intended for internal use only
}
export type ThreadStreamState = {
[threadId: string]: undefined | {
error?: { message: string, fullError: Error | null, };
messageSoFar?: string;
streamingToken?: string;
}
}
const newThreadObject = () => {
const now = new Date().toISOString()
return {
id: new Date().getTime().toString(),
createdAt: now,
lastModified: now,
messages: [],
state: {
stagingSelections: [],
focusedMessageIdx: undefined,
isCheckedOfSelectionId: {}
},
} satisfies ChatThreads[string]
}
const THREAD_VERSION_KEY = 'void.chatThreadVersion'
const THREAD_VERSION = 'v2'
const THREAD_STORAGE_KEY = 'void.chatThreadStorage'
export interface IChatThreadService {
readonly _serviceBrand: undefined;
readonly state: ThreadsState;
readonly streamState: ThreadStreamState;
onDidChangeCurrentThread: Event<void>;
onDidChangeStreamState: Event<{ threadId: string }>
getCurrentThread(): ChatThreads[string];
openNewThread(): void;
switchToThread(threadId: string): void;
getFocusedMessageIdx(): number | undefined;
isFocusingMessage(): boolean;
setFocusedMessageIdx(messageIdx: number | undefined): void;
// _useFocusedStagingState(messageIdx?: number | undefined): readonly [StagingInfo, (stagingInfo: StagingInfo) => void];
_useCurrentThreadState(): readonly [ThreadType['state'], (newState: Partial<ThreadType['state']>) => void];
_useCurrentMessageState(messageIdx: number): readonly [UserMessageState, (newState: Partial<UserMessageState>) => void];
editUserMessageAndStreamResponse(userMessage: string, messageIdx: number): Promise<void>;
addUserMessageAndStreamResponse(userMessage: string): Promise<void>;
cancelStreaming(threadId: string): void;
dismissStreamError(threadId: string): void;
}
export const IChatThreadService = createDecorator<IChatThreadService>('voidChatThreadService');
class ChatThreadService extends Disposable implements IChatThreadService {
_serviceBrand: undefined;
// this fires when the current thread changes at all (a switch of currentThread, or a message added to it, etc)
private readonly _onDidChangeCurrentThread = new Emitter<void>();
readonly onDidChangeCurrentThread: Event<void> = this._onDidChangeCurrentThread.event;
readonly streamState: ThreadStreamState = {}
private readonly _onDidChangeStreamState = new Emitter<{ threadId: string }>();
readonly onDidChangeStreamState: Event<{ threadId: string }> = this._onDidChangeStreamState.event;
state: ThreadsState // allThreads is persisted, currentThread is not
constructor(
@IStorageService private readonly _storageService: IStorageService,
@IModelService private readonly _modelService: IModelService,
@IFileService private readonly _fileService: IFileService,
@ILLMMessageService private readonly _llmMessageService: ILLMMessageService,
) {
super()
this.state = {
allThreads: this._readAllThreads(),
currentThreadId: null as unknown as string, // gets set in startNewThread()
}
// always be in a thread
this.openNewThread()
// for now just write the version, anticipating bigger changes in the future where we'll want to access this
this._storageService.store(THREAD_VERSION_KEY, THREAD_VERSION, StorageScope.APPLICATION, StorageTarget.USER)
}
private _readAllThreads(): ChatThreads {
// PUT ANY VERSION CHANGE FORMAT CONVERSION CODE HERE
// CAN ADD "v0" TAG IN STORAGE AND CONVERT
const threadsStr = this._storageService.get(THREAD_STORAGE_KEY, StorageScope.APPLICATION)
const threads: ChatThreads = threadsStr ? JSON.parse(threadsStr) : {}
this._updateThreadsToVersion(threads, THREAD_VERSION)
return threads
}
private _updateThreadsToVersion(oldThreadsObject: any, toVersion: string) {
if (toVersion === 'v2') {
const threads: ChatThreads = oldThreadsObject
/** v1 -> v2
- threads.state.currentStagingSelections: CodeStagingSelection[] | null;
+ thread[threadIdx].state
+ message.state
*/
// check if we need to update
let shouldUpdate = false
for (const thread of Object.values(threads)) {
if (!thread.state) {
shouldUpdate = true
}
for (const chatMessage of Object.values(thread.messages)) {
if (chatMessage.role === 'user' && !chatMessage.state) {
shouldUpdate = true
}
}
}
if (!shouldUpdate) return;
// update the threads
for (const thread of Object.values(threads)) {
if (!thread.state) {
thread.state = defaultThreadState
}
for (const chatMessage of Object.values(thread.messages)) {
if (chatMessage.role === 'user' && !chatMessage.state) {
chatMessage.state = defaultMessageState
}
}
}
// push the update
this._storeAllThreads(threads)
}
}
private _storeAllThreads(threads: ChatThreads) {
this._storageService.store(THREAD_STORAGE_KEY, JSON.stringify(threads), StorageScope.APPLICATION, StorageTarget.USER)
}
// this should be the only place this.state = ... appears besides constructor
private _setState(state: Partial<ThreadsState>, affectsCurrent: boolean) {
this.state = {
...this.state,
...state
}
if (affectsCurrent)
this._onDidChangeCurrentThread.fire()
}
private _getAllSelections() {
const thread = this.getCurrentThread()
return thread.messages.flatMap(m => m.role === 'user' && m.selections || [])
}
private _getSelectionsUpToMessageIdx(messageIdx: number) {
const thread = this.getCurrentThread()
const prevMessages = thread.messages.slice(0, messageIdx)
return prevMessages.flatMap(m => m.role === 'user' && m.selections || [])
}
private _setStreamState(threadId: string, state: Partial<NonNullable<ThreadStreamState[string]>>) {
this.streamState[threadId] = {
...this.streamState[threadId],
...state
}
this._onDidChangeStreamState.fire({ threadId })
}
// ---------- streaming ----------
finishStreaming = (threadId: string, content: string, error?: { message: string, fullError: Error | null }) => {
// add assistant's message to chat history, and clear selection
const assistantHistoryElt: ChatMessage = { role: 'assistant', content, displayContent: content || null }
this._addMessageToThread(threadId, assistantHistoryElt)
this._setStreamState(threadId, { messageSoFar: undefined, streamingToken: undefined, error })
}
async editUserMessageAndStreamResponse(userMessage: string, messageIdx: number) {
const thread = this.getCurrentThread()
if (thread.messages?.[messageIdx]?.role !== 'user') {
throw new Error("Error: editing a message with role !=='user'")
}
// get prev and curr selections before clearing the message
const prevSelns = this._getSelectionsUpToMessageIdx(messageIdx)
const currSelns = thread.messages[messageIdx].selections || []
// clear messages up to the index
const slicedMessages = thread.messages.slice(0, messageIdx)
this._setState({
allThreads: {
...this.state.allThreads,
[thread.id]: {
...thread,
messages: slicedMessages
}
}
}, true)
// stream the edit
this.addUserMessageAndStreamResponse(userMessage, { prevSelns, currSelns })
}
async addUserMessageAndStreamResponse(userMessage: string, options?: { prevSelns?: StagingSelectionItem[], currSelns?: StagingSelectionItem[] }) {
const thread = this.getCurrentThread()
const threadId = thread.id
// add user's message to chat history
const instructions = userMessage
const prevSelns: StagingSelectionItem[] = options?.prevSelns ?? this._getAllSelections()
const currSelns: StagingSelectionItem[] = options?.currSelns ?? thread.state.stagingSelections
// read all curr+previous files on demand instead of adding them to the history
const messageContent = await chat_userMessageContent(instructions, prevSelns, currSelns)
const messageContentWithAllFiles = await chat_userMessageContentWithAllFiles(instructions, prevSelns, currSelns, this._modelService, this._fileService)
const prevLLMMessages = this.getCurrentThread().messages.map(m => ({ role: m.role, content: m.content || '(empty model output)' }))
const currLLMMessage: LLMChatMessage = { role: 'user', content: messageContentWithAllFiles }
const userHistoryElt: ChatMessage = { role: 'user', content: messageContent, displayContent: instructions, selections: currSelns, state: defaultMessageState }
this._addMessageToThread(threadId, userHistoryElt)
this._setStreamState(threadId, { error: undefined })
console.log(`messageContent`)
console.log([{ role: 'system', content: chat_systemMessage },
...prevLLMMessages,
currLLMMessage,])
const llmCancelToken = this._llmMessageService.sendLLMMessage({
messagesType: 'chatMessages',
logging: { loggingName: 'Chat' },
useProviderFor: 'Ctrl+L',
messages: [
{ role: 'system', content: chat_systemMessage },
...prevLLMMessages,
currLLMMessage,
],
onText: ({ newText, fullText }) => {
this._setStreamState(threadId, { messageSoFar: fullText })
},
onFinalMessage: ({ fullText: content }) => {
this.finishStreaming(threadId, content)
},
onError: (error) => {
this.finishStreaming(threadId, this.streamState[threadId]?.messageSoFar ?? '', error)
},
})
if (llmCancelToken === null) return
this._setStreamState(threadId, { streamingToken: llmCancelToken })
}
cancelStreaming(threadId: string) {
const llmCancelToken = this.streamState[threadId]?.streamingToken
if (llmCancelToken !== undefined) this._llmMessageService.abort(llmCancelToken)
this.finishStreaming(threadId, this.streamState[threadId]?.messageSoFar ?? '')
}
dismissStreamError(threadId: string): void {
this._setStreamState(threadId, { error: undefined })
}
// ---------- the rest ----------
getCurrentThread(): ChatThreads[string] {
const state = this.state
return state.allThreads[state.currentThreadId]
}
getFocusedMessageIdx() {
const thread = this.getCurrentThread()
// get the focusedMessageIdx
const focusedMessageIdx = thread.state.focusedMessageIdx
if (focusedMessageIdx === undefined) return;
// check that the message is actually being edited
const focusedMessage = thread.messages[focusedMessageIdx]
if (focusedMessage.role !== 'user') return;
if (!focusedMessage.state) return;
return focusedMessageIdx
}
isFocusingMessage() {
return this.getFocusedMessageIdx() !== undefined
}
switchToThread(threadId: string) {
this._setState({ currentThreadId: threadId }, true)
}
openNewThread() {
// if a thread with 0 messages already exists, switch to it
const { allThreads: currentThreads } = this.state
for (const threadId in currentThreads) {
if (currentThreads[threadId].messages.length === 0) {
this.switchToThread(threadId)
return
}
}
// otherwise, start a new thread
const newThread = newThreadObject()
// update state
const newThreads: ChatThreads = {
...currentThreads,
[newThread.id]: newThread
}
this._storeAllThreads(newThreads)
this._setState({ allThreads: newThreads, currentThreadId: newThread.id }, true)
}
_addMessageToThread(threadId: string, message: ChatMessage) {
const { allThreads } = this.state
const oldThread = allThreads[threadId]
// update state and store it
const newThreads = {
...allThreads,
[oldThread.id]: {
...oldThread,
lastModified: new Date().toISOString(),
messages: [...oldThread.messages, message],
}
}
this._storeAllThreads(newThreads)
this._setState({ allThreads: newThreads }, true) // the current thread just changed (it had a message added to it)
}
// sets the currently selected message (must be undefined if no message is selected)
setFocusedMessageIdx(messageIdx: number | undefined) {
const threadId = this.state.currentThreadId
const thread = this.state.allThreads[threadId]
if (!thread) return
this._setState({
allThreads: {
...this.state.allThreads,
[threadId]: {
...thread,
state: {
...thread.state,
focusedMessageIdx: messageIdx,
}
}
}
}, true)
}
// set message.state
private _setCurrentMessageState(state: Partial<UserMessageState>, messageIdx: number): void {
const threadId = this.state.currentThreadId
const thread = this.state.allThreads[threadId]
if (!thread) return
this._setState({
allThreads: {
...this.state.allThreads,
[threadId]: {
...thread,
messages: thread.messages.map((m, i) =>
i === messageIdx && m.role === 'user' ? {
...m,
state: {
...m.state,
...state
},
} : m
)
}
}
}, true)
}
// set thread.state
private _setCurrentThreadState(state: Partial<ThreadType['state']>): void {
const threadId = this.state.currentThreadId
const thread = this.state.allThreads[threadId]
if (!thread) return
this._setState({
allThreads: {
...this.state.allThreads,
[thread.id]: {
...thread,
state: {
...thread.state,
...state
}
}
}
}, true)
}
_useCurrentMessageState(messageIdx: number) {
const thread = this.getCurrentThread()
const messages = thread.messages
const currMessage = messages[messageIdx]
if (currMessage.role !== 'user') {
return [defaultMessageState, (s: any) => { }] as const
}
const state = currMessage.state
const setState = (newState: Partial<UserMessageState>) => this._setCurrentMessageState(newState, messageIdx)
return [state, setState] as const
}
_useCurrentThreadState() {
const thread = this.getCurrentThread()
const state = thread.state
const setState = this._setCurrentThreadState.bind(this)
return [state, setState] as const
}
}
registerSingleton(IChatThreadService, ChatThreadService, InstantiationType.Eager);
@@ -1,354 +0,0 @@
import { CancellationToken } from '../../../../base/common/cancellation.js';
import { Position } from '../../../../editor/common/core/position.js';
import { DocumentSymbol, SymbolKind } from '../../../../editor/common/languages.js';
import { ITextModel } from '../../../../editor/common/model.js';
import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { Range, IRange } from '../../../../editor/common/core/range.js';
import { Disposable } from '../../../../base/common/lifecycle.js';
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
import { IModelService } from '../../../../editor/common/services/model.js';
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
import { URI } from '../../../../base/common/uri.js';
// make sure snippet logic works
// change logic for `visited` to intervals
// atomically set new snippets at end
// throttle cache setting
interface IVisitedInterval {
uri: string;
startLine: number;
endLine: number;
}
export interface IContextGatheringService {
readonly _serviceBrand: undefined;
updateCache(model: ITextModel, pos: Position): Promise<void>;
getCachedSnippets(): string[];
}
export const IContextGatheringService = createDecorator<IContextGatheringService>('contextGatheringService');
class ContextGatheringService extends Disposable implements IContextGatheringService {
_serviceBrand: undefined;
private readonly _NUM_LINES = 3;
private readonly _MAX_SNIPPET_LINES = 7; // Reasonable size for context
// Cache holds the most recent list of snippets.
private _cache: string[] = [];
private _snippetIntervals: IVisitedInterval[] = [];
constructor(
@ILanguageFeaturesService private readonly _langFeaturesService: ILanguageFeaturesService,
@IModelService private readonly _modelService: IModelService,
@ICodeEditorService private readonly _codeEditorService: ICodeEditorService
) {
super();
this._modelService.getModels().forEach(model => this._subscribeToModel(model));
this._register(this._modelService.onModelAdded(model => this._subscribeToModel(model)));
}
private _subscribeToModel(model: ITextModel): void {
console.log("Subscribing to model:", model.uri.toString());
this._register(model.onDidChangeContent(() => {
const editor = this._codeEditorService.getFocusedCodeEditor();
if (editor && editor.getModel() === model) {
const pos = editor.getPosition();
console.log("updateCache called at position:", pos);
if (pos) {
this.updateCache(model, pos);
}
}
}));
}
public async updateCache(model: ITextModel, pos: Position): Promise<void> {
const snippets = new Set<string>();
this._snippetIntervals = []; // Reset intervals for new cache update
await this._gatherNearbySnippets(model, pos, this._NUM_LINES, 3, snippets, this._snippetIntervals);
await this._gatherParentSnippets(model, pos, this._NUM_LINES, 3, snippets, this._snippetIntervals);
// Convert to array and filter overlapping snippets
this._cache = Array.from(snippets);
console.log("Cache updated:", this._cache);
}
public getCachedSnippets(): string[] {
return this._cache;
}
// Basic snippet extraction.
private _getSnippetForRange(model: ITextModel, range: IRange, numLines: number): string {
const startLine = Math.max(range.startLineNumber - numLines, 1);
const endLine = Math.min(range.endLineNumber + numLines, model.getLineCount());
// Enforce maximum snippet size
const totalLines = endLine - startLine + 1;
const adjustedStartLine = totalLines > this._MAX_SNIPPET_LINES
? endLine - this._MAX_SNIPPET_LINES + 1
: startLine;
const snippetRange = new Range(adjustedStartLine, 1, endLine, model.getLineMaxColumn(endLine));
return this._cleanSnippet(model.getValueInRange(snippetRange));
}
private _cleanSnippet(snippet: string): string {
return snippet
.split('\n')
// Remove empty lines and lines with only comments
.filter(line => {
const trimmed = line.trim();
return trimmed && !/^\/\/+$/.test(trimmed);
})
// Rejoin with newlines
.join('\n')
// Remove excess whitespace
.trim();
}
private _normalizeSnippet(snippet: string): string {
return snippet
// Remove multiple newlines
.replace(/\n{2,}/g, '\n')
// Remove trailing whitespace
.trim();
}
private _addSnippetIfNotOverlapping(
model: ITextModel,
range: IRange,
snippets: Set<string>,
visited: IVisitedInterval[]
): void {
const startLine = range.startLineNumber;
const endLine = range.endLineNumber;
const uri = model.uri.toString();
if (!this._isRangeVisited(uri, startLine, endLine, visited)) {
visited.push({ uri, startLine, endLine });
const snippet = this._normalizeSnippet(this._getSnippetForRange(model, range, this._NUM_LINES));
if (snippet.length > 0) {
snippets.add(snippet);
}
}
}
private async _gatherNearbySnippets(
model: ITextModel,
pos: Position,
numLines: number,
depth: number,
snippets: Set<string>,
visited: IVisitedInterval[]
): Promise<void> {
if (depth <= 0) return;
const startLine = Math.max(pos.lineNumber - numLines, 1);
const endLine = Math.min(pos.lineNumber + numLines, model.getLineCount());
const range = new Range(startLine, 1, endLine, model.getLineMaxColumn(endLine));
this._addSnippetIfNotOverlapping(model, range, snippets, visited);
const symbols = await this._getSymbolsNearPosition(model, pos, numLines);
for (const sym of symbols) {
const defs = await this._getDefinitionSymbols(model, sym);
for (const def of defs) {
const defModel = this._modelService.getModel(def.uri);
if (defModel) {
const defPos = new Position(def.range.startLineNumber, def.range.startColumn);
this._addSnippetIfNotOverlapping(defModel, def.range, snippets, visited);
await this._gatherNearbySnippets(defModel, defPos, numLines, depth - 1, snippets, visited);
}
}
}
}
private async _gatherParentSnippets(
model: ITextModel,
pos: Position,
numLines: number,
depth: number,
snippets: Set<string>,
visited: IVisitedInterval[]
): Promise<void> {
if (depth <= 0) return;
const container = await this._findContainerFunction(model, pos);
if (!container) return;
const containerRange = container.kind === SymbolKind.Method ? container.selectionRange : container.range;
this._addSnippetIfNotOverlapping(model, containerRange, snippets, visited);
const symbols = await this._getSymbolsNearRange(model, containerRange, numLines);
for (const sym of symbols) {
const defs = await this._getDefinitionSymbols(model, sym);
for (const def of defs) {
const defModel = this._modelService.getModel(def.uri);
if (defModel) {
const defPos = new Position(def.range.startLineNumber, def.range.startColumn);
this._addSnippetIfNotOverlapping(defModel, def.range, snippets, visited);
await this._gatherNearbySnippets(defModel, defPos, numLines, depth - 1, snippets, visited);
}
}
}
const containerPos = new Position(containerRange.startLineNumber, containerRange.startColumn);
await this._gatherParentSnippets(model, containerPos, numLines, depth - 1, snippets, visited);
}
private _isRangeVisited(uri: string, startLine: number, endLine: number, visited: IVisitedInterval[]): boolean {
return visited.some(interval =>
interval.uri === uri &&
!(endLine < interval.startLine || startLine > interval.endLine)
);
}
private async _getSymbolsNearPosition(model: ITextModel, pos: Position, numLines: number): Promise<DocumentSymbol[]> {
const startLine = Math.max(pos.lineNumber - numLines, 1);
const endLine = Math.min(pos.lineNumber + numLines, model.getLineCount());
const range = new Range(startLine, 1, endLine, model.getLineMaxColumn(endLine));
return this._getSymbolsInRange(model, range);
}
private async _getSymbolsNearRange(model: ITextModel, range: IRange, numLines: number): Promise<DocumentSymbol[]> {
const centerLine = Math.floor((range.startLineNumber + range.endLineNumber) / 2);
const startLine = Math.max(centerLine - numLines, 1);
const endLine = Math.min(centerLine + numLines, model.getLineCount());
const searchRange = new Range(startLine, 1, endLine, model.getLineMaxColumn(endLine));
return this._getSymbolsInRange(model, searchRange);
}
private async _getSymbolsInRange(model: ITextModel, range: IRange): Promise<DocumentSymbol[]> {
const symbols: DocumentSymbol[] = [];
const providers = this._langFeaturesService.documentSymbolProvider.ordered(model);
for (const provider of providers) {
try {
const result = await provider.provideDocumentSymbols(model, CancellationToken.None);
if (result) {
const flat = this._flattenSymbols(result);
const intersecting = flat.filter(sym => this._rangesIntersect(sym.range, range));
symbols.push(...intersecting);
}
} catch (e) {
console.warn("Symbol provider error:", e);
}
}
// Also check reference providers.
const refProviders = this._langFeaturesService.referenceProvider.ordered(model);
for (let line = range.startLineNumber; line <= range.endLineNumber; line++) {
const content = model.getLineContent(line);
const words = content.match(/[a-zA-Z_]\w*/g) || [];
for (const word of words) {
const startColumn = content.indexOf(word) + 1;
const pos = new Position(line, startColumn);
if (!this._positionInRange(pos, range)) continue;
for (const provider of refProviders) {
try {
const refs = await provider.provideReferences(model, pos, { includeDeclaration: true }, CancellationToken.None);
if (refs) {
const filtered = refs.filter(ref => this._rangesIntersect(ref.range, range));
for (const ref of filtered) {
symbols.push({
name: word,
detail: '',
kind: SymbolKind.Variable,
range: ref.range,
selectionRange: ref.range,
children: [],
tags: []
});
}
}
} catch (e) {
console.warn("Reference provider error:", e);
}
}
}
}
return symbols;
}
private _flattenSymbols(symbols: DocumentSymbol[]): DocumentSymbol[] {
const flat: DocumentSymbol[] = [];
for (const sym of symbols) {
flat.push(sym);
if (sym.children && sym.children.length > 0) {
flat.push(...this._flattenSymbols(sym.children));
}
}
return flat;
}
private _rangesIntersect(a: IRange, b: IRange): boolean {
return !(
a.endLineNumber < b.startLineNumber ||
a.startLineNumber > b.endLineNumber ||
(a.endLineNumber === b.startLineNumber && a.endColumn < b.startColumn) ||
(a.startLineNumber === b.endLineNumber && a.endColumn > b.endColumn)
);
}
private _positionInRange(pos: Position, range: IRange): boolean {
return pos.lineNumber >= range.startLineNumber &&
pos.lineNumber <= range.endLineNumber &&
(pos.lineNumber !== range.startLineNumber || pos.column >= range.startColumn) &&
(pos.lineNumber !== range.endLineNumber || pos.column <= range.endColumn);
}
// Get definition symbols for a given symbol.
private async _getDefinitionSymbols(model: ITextModel, symbol: DocumentSymbol): Promise<(DocumentSymbol & { uri: URI })[]> {
const pos = new Position(symbol.range.startLineNumber, symbol.range.startColumn);
const providers = this._langFeaturesService.definitionProvider.ordered(model);
const defs: (DocumentSymbol & { uri: URI })[] = [];
for (const provider of providers) {
try {
const res = await provider.provideDefinition(model, pos, CancellationToken.None);
if (res) {
const links = Array.isArray(res) ? res : [res];
defs.push(...links.map(link => ({
name: symbol.name,
detail: symbol.detail,
kind: symbol.kind,
range: link.range,
selectionRange: link.range,
children: [],
tags: symbol.tags || [],
uri: link.uri // Now keeping it as URI instead of converting to string
})));
}
} catch (e) {
console.warn("Definition provider error:", e);
}
}
return defs;
}
private async _findContainerFunction(model: ITextModel, pos: Position): Promise<DocumentSymbol | null> {
const searchRange = new Range(
Math.max(pos.lineNumber - 1, 1), 1,
Math.min(pos.lineNumber + 1, model.getLineCount()),
model.getLineMaxColumn(pos.lineNumber)
);
const symbols = await this._getSymbolsInRange(model, searchRange);
const funcs = symbols.filter(s =>
(s.kind === SymbolKind.Function || s.kind === SymbolKind.Method) &&
this._positionInRange(pos, s.range)
);
if (!funcs.length) return null;
return funcs.reduce((innermost, current) => {
if (!innermost) return current;
const moreInner =
(current.range.startLineNumber > innermost.range.startLineNumber ||
(current.range.startLineNumber === innermost.range.startLineNumber &&
current.range.startColumn > innermost.range.startColumn)) &&
(current.range.endLineNumber < innermost.range.endLineNumber ||
(current.range.endLineNumber === innermost.range.endLineNumber &&
current.range.endColumn < innermost.range.endColumn));
return moreInner ? current : innermost;
}, null as DocumentSymbol | null);
}
}
registerSingleton(IContextGatheringService, ContextGatheringService, InstantiationType.Eager);
@@ -1,7 +1,7 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
*-----------------------------------------------------------------------------------------*/
import { Disposable } from '../../../../../base/common/lifecycle.js';
import { URI } from '../../../../../base/common/uri.js';
@@ -80,7 +80,6 @@ export class ConsistentItemService extends Disposable {
}
const initializeEditor = (editor: ICodeEditor) => {
// if (editor.getModel()?.uri.scheme !== 'file') return // THIS BREAKS THINGS
addTabSwitchListeners(editor)
addDisposeListener(editor)
putItemsOnEditor(editor, editor.getModel()?.uri ?? null)
@@ -91,7 +90,10 @@ export class ConsistentItemService extends Disposable {
this._register(this._editorService.onCodeEditorAdd(editor => { initializeEditor(editor) }))
// when an editor is deleted, remove its items
this._register(this._editorService.onCodeEditorRemove(editor => { removeItemsFromEditor(editor) }))
this._register(this._editorService.onCodeEditorRemove(editor => {
removeItemsFromEditor(editor)
}))
}
@@ -155,6 +157,7 @@ export class ConsistentItemService extends Disposable {
removeConsistentItemFromURI(consistentItemId: string) {
if (!(consistentItemId in this.infoOfConsistentItemId))
return
@@ -170,7 +173,6 @@ export class ConsistentItemService extends Disposable {
// clear
this.consistentItemIdsOfURI[uri.fsPath]?.delete(consistentItemId)
delete this.infoOfConsistentItemId[consistentItemId]
}
@@ -1,170 +0,0 @@
// eg "bash" -> "shell"
export const nameToVscodeLanguage: { [key: string]: string } = {
// Web Technologies
'html': 'html',
'css': 'css',
'scss': 'scss',
'sass': 'scss',
'less': 'less',
'javascript': 'typescript',
'js': 'typescript', // use more general renderer
'jsx': 'typescript',
'typescript': 'typescript',
'ts': 'typescript',
'tsx': 'typescript',
'json': 'json',
'jsonc': 'json',
// Programming Languages
'python': 'python',
'py': 'python',
'java': 'java',
'cpp': 'cpp',
'c++': 'cpp',
'c': 'c',
'csharp': 'csharp',
'cs': 'csharp',
'c#': 'csharp',
'go': 'go',
'golang': 'go',
'rust': 'rust',
'rs': 'rust',
'ruby': 'ruby',
'rb': 'ruby',
'php': 'php',
'shell': 'shell',
'bash': 'shell',
'sh': 'shell',
'zsh': 'shell',
// Markup and Config
'markdown': 'markdown',
'md': 'markdown',
'xml': 'xml',
'svg': 'xml',
'yaml': 'yaml',
'yml': 'yaml',
'ini': 'ini',
'toml': 'ini',
// Database and Query Languages
'sql': 'sql',
'mysql': 'sql',
'postgresql': 'sql',
'graphql': 'graphql',
'gql': 'graphql',
// Others
'dockerfile': 'dockerfile',
'docker': 'dockerfile',
'makefile': 'makefile',
'plaintext': 'plaintext',
'text': 'plaintext'
};
// eg ".ts" -> "typescript"
const fileExtensionToVscodeLanguage: { [key: string]: string } = {
// Web
'html': 'html',
'htm': 'html',
'css': 'css',
'scss': 'scss',
'less': 'less',
'js': 'javascript',
'jsx': 'javascript',
'ts': 'typescript',
'tsx': 'typescript',
'json': 'json',
'jsonc': 'json',
// Programming Languages
'py': 'python',
'java': 'java',
'cpp': 'cpp',
'cc': 'cpp',
'c': 'c',
'h': 'cpp',
'hpp': 'cpp',
'cs': 'csharp',
'go': 'go',
'rs': 'rust',
'rb': 'ruby',
'php': 'php',
'sh': 'shell',
'bash': 'shell',
'zsh': 'shell',
// Markup/Config
'md': 'markdown',
'markdown': 'markdown',
'xml': 'xml',
'svg': 'xml',
'yaml': 'yaml',
'yml': 'yaml',
'ini': 'ini',
'toml': 'ini',
// Other
'sql': 'sql',
'graphql': 'graphql',
'gql': 'graphql',
'dockerfile': 'dockerfile',
'docker': 'dockerfile',
'mk': 'makefile',
// Config Files and Dot Files
'npmrc': 'ini',
'env': 'ini',
'gitignore': 'ignore',
'dockerignore': 'ignore',
'eslintrc': 'json',
'babelrc': 'json',
'prettierrc': 'json',
'stylelintrc': 'json',
'editorconfig': 'ini',
'htaccess': 'apacheconf',
'conf': 'ini',
'config': 'ini',
// Package Files
'package': 'json',
'package-lock': 'json',
'gemfile': 'ruby',
'podfile': 'ruby',
'rakefile': 'ruby',
// Build Systems
'cmake': 'cmake',
'makefile': 'makefile',
'gradle': 'groovy',
// Shell Scripts
'bashrc': 'shell',
'zshrc': 'shell',
'fish': 'shell',
// Version Control
'gitconfig': 'ini',
'hgrc': 'ini',
'svnconfig': 'ini',
// Web Server
'nginx': 'nginx',
// Misc Config
'properties': 'properties',
'cfg': 'ini',
'reg': 'ini'
};
export function filenameToVscodeLanguage(filename: string): string | undefined {
const ext = filename.toLowerCase().split('.').pop();
if (!ext) return undefined;
return fileExtensionToVscodeLanguage[ext];
}
@@ -1,177 +1,18 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
*-----------------------------------------------------------------------------------------*/
class SurroundingsRemover {
readonly originalS: string
i: number
j: number
export const extractCodeFromResult = (result: string) => {
// Match either:
// 1. ```language\n<code>```
// 2. ```<code>```
const match = result.match(/```(?:\w+\n)?([\s\S]*?)```|```([\s\S]*?)```/);
// string is s[i...j]
constructor(s: string) {
this.originalS = s
this.i = 0
this.j = s.length - 1
}
value() {
return this.originalS.substring(this.i, this.j + 1)
if (!match) {
return result;
}
// returns whether it removed the whole prefix
removePrefix = (prefix: string): boolean => {
let offset = 0
// console.log('prefix', prefix, Math.min(this.j, prefix.length - 1))
while (this.i <= this.j && offset <= prefix.length - 1) {
if (this.originalS.charAt(this.i) !== prefix.charAt(offset))
break
offset += 1
this.i += 1
}
return offset === prefix.length
}
// // removes suffix from right to left
removeSuffix = (suffix: string): boolean => {
// e.g. suffix = <PRE/>, the string is <PRE>hi<P
const s = this.value()
// for every possible prefix of `suffix`, check if string ends with it
for (let len = Math.min(s.length, suffix.length); len >= 1; len -= 1) {
if (s.endsWith(suffix.substring(0, len))) { // the end of the string equals a prefix
this.j -= len
return len === suffix.length
}
}
return false
}
// removeSuffix = (suffix: string): boolean => {
// let offset = 0
// while (this.j >= Math.max(this.i, 0)) {
// if (this.originalS.charAt(this.j) !== suffix.charAt(suffix.length - 1 - offset))
// break
// offset += 1
// this.j -= 1
// }
// return offset === suffix.length
// }
removeFromStartUntil = (until: string, alsoRemoveUntilStr: boolean) => {
const index = this.originalS.indexOf(until, this.i)
if (index === -1) {
this.i = this.j + 1
return false
}
// console.log('index', index, until.length)
if (alsoRemoveUntilStr)
this.i = index + until.length
else
this.i = index
return true
}
removeCodeBlock = () => {
// Match either:
// 1. ```language\n<code>\n```\n?
// 2. ```<code>\n```\n?
const pm = this
const foundCodeBlock = pm.removePrefix('```')
if (!foundCodeBlock) return false
pm.removeFromStartUntil('\n', true) // language
const j = pm.j
let foundCodeBlockEnd = pm.removeSuffix('```')
if (pm.j === j) foundCodeBlockEnd = pm.removeSuffix('```\n') // if no change, try again with \n after ```
if (!foundCodeBlockEnd) return false
pm.removeSuffix('\n') // remove the newline before ```
return true
}
deltaInfo = (recentlyAddedTextLen: number) => {
// aaaaaatextaaaaaa{recentlyAdded}
// ^ i j len
// |
// recentyAddedIdx
const recentlyAddedIdx = this.originalS.length - recentlyAddedTextLen
const actualDelta = this.originalS.substring(Math.max(this.i, recentlyAddedIdx), this.j + 1)
const ignoredSuffix = this.originalS.substring(Math.max(this.j + 1, recentlyAddedIdx), Infinity)
return [actualDelta, ignoredSuffix] as const
}
// Return whichever group matched (non-empty)
return match[1] ?? match[2] ?? result;
}
export const extractCodeFromRegular = ({ text, recentlyAddedTextLen }: { text: string, recentlyAddedTextLen: number }): [string, string, string] => {
const pm = new SurroundingsRemover(text)
pm.removeCodeBlock()
const s = pm.value()
const [delta, ignoredSuffix] = pm.deltaInfo(recentlyAddedTextLen)
return [s, delta, ignoredSuffix]
}
// Ollama has its own FIM, we should not use this if we use that
export const extractCodeFromFIM = ({ text, recentlyAddedTextLen, midTag, }: { text: string, recentlyAddedTextLen: number, midTag: string }): [string, string, string] => {
/* ------------- summary of the regex -------------
[optional ` | `` | ```]
(match optional_language_name)
[optional strings here]
[required <MID> tag]
(match the stuff between mid tags)
[optional <MID/> tag]
[optional ` | `` | ```]
*/
const pm = new SurroundingsRemover(text)
pm.removeCodeBlock()
const foundMid = pm.removePrefix(`<${midTag}>`)
if (foundMid) {
pm.removeSuffix(`</${midTag}>`)
}
const s = pm.value()
const [delta, ignoredSuffix] = pm.deltaInfo(recentlyAddedTextLen)
return [s, delta, ignoredSuffix]
// // const regex = /[\s\S]*?(?:`{1,3}\s*([a-zA-Z_]+[\w]*)?[\s\S]*?)?<MID>([\s\S]*?)(?:<\/MID>|`{1,3}|$)/;
// const regex = new RegExp(
// `[\\s\\S]*?(?:\`{1,3}\\s*([a-zA-Z_]+[\\w]*)?[\\s\\S]*?)?<${midTag}>([\\s\\S]*?)(?:</${midTag}>|\`{1,3}|$)`,
// ''
// );
// const match = text.match(regex);
// if (match) {
// const [_, languageName, codeBetweenMidTags] = match;
// return [languageName, codeBetweenMidTags] as const
// } else {
// return [undefined, extractCodeFromRegular(text)] as const
// }
}
@@ -1,7 +1,7 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
*-----------------------------------------------------------------------------------------*/
import { diffLines } from '../react/out/diff/index.js'
@@ -0,0 +1,20 @@
/*------------------------------------------------------------------------------------------
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
*-----------------------------------------------------------------------------------------*/
import { isMacintosh } from '../../../../../base/common/platform.js';
// import { OperatingSystem, OS } from '../../../../base/common/platform.js';
// OS === OperatingSystem.Macintosh
export function getCmdKey(): string {
if (isMacintosh) {
return '⌘';
} else {
return 'Ctrl';
}
}
@@ -1,44 +0,0 @@
import { URI } from '../../../../../base/common/uri'
import { EndOfLinePreference } from '../../../../../editor/common/model'
import { IModelService } from '../../../../../editor/common/services/model.js'
import { IFileService } from '../../../../../platform/files/common/files'
// attempts to read URI of currently opened model, then of raw file
export const VSReadFile = async (modelService: IModelService, fileService: IFileService, uri: URI) => {
const modelResult = await _VSReadModel(modelService, uri)
if (modelResult) return modelResult
const fileResult = await _VSReadFileRaw(fileService, uri)
if (fileResult) return fileResult
return ''
}
// read files from VSCode. preferred (but appears to only work if the model of this URI already exists. If it doesn't use the other function.)
export const _VSReadModel = async (modelService: IModelService, uri: URI): Promise<string | null> => {
// attempt to read saved model (sometimes doesn't work if page is reloaded)
const model = modelService.getModel(uri)
if (model) {
return model.getValue(EndOfLinePreference.LF)
}
// look at all opened models and check if they have the same `fsPath`
const models = modelService.getModels();
for (const model of models) {
if (model.uri.fsPath.toString() === uri.fsPath.toString()) {
return model.getValue(EndOfLinePreference.LF);
}
}
return null
}
export const _VSReadFileRaw = async (fileService: IFileService, uri: URI) => {
const res = await fileService.readFile(uri)
const str = res.value.toString()
return str
}
File diff suppressed because it is too large Load Diff
@@ -1,7 +1,7 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
*-----------------------------------------------------------------------------------------*/
.monaco-editor .void-sweepIdxBG {
background-color: var(--vscode-void-sweepIdxBG);
@@ -70,99 +70,4 @@
.void-link {
color: #3b82f6;
cursor: pointer;
transition: all 0.2s ease;
}
.void-link:hover {
opacity: 80%;
}
.void-scrollable-element::-webkit-scrollbar,
.void-scrollable-element *::-webkit-scrollbar {
width: 14px !important;
height: 14px !important;
}
.void-scrollable-element::-webkit-scrollbar-track,
.void-scrollable-element *::-webkit-scrollbar-track {
background: transparent !important;
}
.void-scrollable-element::-webkit-scrollbar-thumb,
.void-scrollable-element *::-webkit-scrollbar-thumb {
background-color: transparent !important;
border-radius: 0px !important;
}
.void-scrollable-element::-webkit-scrollbar-thumb:hover,
.void-scrollable-element *::-webkit-scrollbar-thumb:hover {
background-color: var(--vscode-scrollbarSlider-hoverBackground) !important;
}
.void-scrollable-element::-webkit-scrollbar-thumb:active,
.void-scrollable-element *::-webkit-scrollbar-thumb:active {
background-color: var(--vscode-scrollbarSlider-activeBackground) !important;
}
.void-scrollable-element::-webkit-scrollbar-corner,
.void-scrollable-element *::-webkit-scrollbar-corner {
background-color: transparent !important;
}
.void-scrollable-element.show-scrollbar-0::-webkit-scrollbar-thumb,
.void-scrollable-element.show-scrollbar-0 *::-webkit-scrollbar-thumb {
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 0%, transparent) !important;
}
.void-scrollable-element.show-scrollbar-1::-webkit-scrollbar-thumb,
.void-scrollable-element.show-scrollbar-1 *::-webkit-scrollbar-thumb {
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 10%, transparent) !important;
}
.void-scrollable-element.show-scrollbar-2::-webkit-scrollbar-thumb,
.void-scrollable-element.show-scrollbar-2 *::-webkit-scrollbar-thumb {
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 20%, transparent) !important;
}
.void-scrollable-element.show-scrollbar-3::-webkit-scrollbar-thumb,
.void-scrollable-element.show-scrollbar-3 *::-webkit-scrollbar-thumb {
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 30%, transparent) !important;
}
.void-scrollable-element.show-scrollbar-4::-webkit-scrollbar-thumb,
.void-scrollable-element.show-scrollbar-4 *::-webkit-scrollbar-thumb {
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 40%, transparent) !important;
}
.void-scrollable-element.show-scrollbar-5::-webkit-scrollbar-thumb,
.void-scrollable-element.show-scrollbar-5 *::-webkit-scrollbar-thumb {
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 50%, transparent) !important;
}
.void-scrollable-element.show-scrollbar-6::-webkit-scrollbar-thumb,
.void-scrollable-element.show-scrollbar-6 *::-webkit-scrollbar-thumb {
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 60%, transparent) !important;
}
.void-scrollable-element.show-scrollbar-7::-webkit-scrollbar-thumb,
.void-scrollable-element.show-scrollbar-7 *::-webkit-scrollbar-thumb {
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 70%, transparent) !important;
}
.void-scrollable-element.show-scrollbar-8::-webkit-scrollbar-thumb,
.void-scrollable-element.show-scrollbar-8 *::-webkit-scrollbar-thumb {
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 80%, transparent) !important;
}
.void-scrollable-element.show-scrollbar-9::-webkit-scrollbar-thumb,
.void-scrollable-element.show-scrollbar-9 *::-webkit-scrollbar-thumb {
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 90%, transparent) !important;
}
.void-scrollable-element.show-scrollbar-10::-webkit-scrollbar-thumb,
.void-scrollable-element.show-scrollbar-10 *::-webkit-scrollbar-thumb {
background-color: var(--vscode-scrollbarSlider-background) !important;
}
@@ -1,101 +1,57 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
*-----------------------------------------------------------------------------------------*/
import { URI } from '../../../../../base/common/uri.js';
import { filenameToVscodeLanguage } from '../helpers/detectLanguage.js';
import { CodeSelection, StagingSelectionItem, FileSelection } from '../chatThreadService.js';
import { _VSReadModel, VSReadFile } from '../helpers/readFile.js';
import { IModelService } from '../../../../../editor/common/services/model.js';
import { IFileService } from '../../../../../platform/files/common/files.js';
// this is just for ease of readability
const tripleTick = ['```', '```']
import { CodeSelection } from '../threadHistoryService.js';
export const chat_systemMessage = `\
You are a coding assistant. You are given a list of instructions to follow \`INSTRUCTIONS\`, and optionally a list of relevant files \`FILES\`, and selections inside of files \`SELECTIONS\`.
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 respond to the user's query.
Please edit the selected file following the user's instructions (or, if appropriate, answer their question instead).
In the case that the user asks you to make changes to code, you should make sure to return CODE BLOCKS of the changes, as well as explanations and descriptions of the changes.
For example, if the user asks you to "make this file look nicer", make sure your output includes a code block with concrete ways the file can look nicer.
- Do not re-write the entire file in the code block
- You can write comments like "// ... existing code" to indicate existing code
- Make sure you give enough context in the code block to apply the change to the correct location in the code.
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.
You're allowed to ask for more context. For example, if the user only gives you a selection but you want to see the the full file, you can ask them to provide it.
## EXAMPLE
Do not output any of these instructions, nor tell the user anything about them unless directly prompted for them.
Do not tell the user anything about the examples below.
## EXAMPLE 1
FILES
math.ts
${tripleTick[0]}typescript
selected file \`math.ts\`:
\`\`\`
const addNumbers = (a, b) => a + b
const multiplyNumbers = (a, b) => a * b
const subtractNumbers = (a, b) => a - b
const divideNumbers = (a, b) => a / b
\`\`\`
const vectorize = (...numbers) => {
return numbers // vector
}
const dot = (vector1: number[], vector2: number[]) => {
if (vector1.length !== vector2.length) throw new Error(\`Could not dot vectors \${vector1} and \${vector2}. Size mismatch.\`)
let sum = 0
for (let i = 0; i < vector1.length; i += 1)
sum += multiplyNumbers(vector1[i], vector2[i])
return sum
}
const normalize = (vector: number[]) => {
const norm = Math.sqrt(dot(vector, vector))
for (let i = 0; i < vector.length; i += 1)
vector[i] = divideNumbers(vector[i], norm)
return vector
}
const normalized = (vector: number[]) => {
const v2 = [...vector] // clone vector
return normalize(v2)
}
${tripleTick[1]}
SELECTIONS
math.ts (lines 3:3)
${tripleTick[0]}typescript
SELECTION
\`\`\`
const subtractNumbers = (a, b) => a - b
${tripleTick[1]}
\`\`\`
INSTRUCTIONS
add a function that exponentiates a number below this, and use it to make a power function that raises all entries of a vector to a power
\`\`\`
add a function that multiplies numbers below this
\`\`\`
ACCEPTED OUTPUT
EXPECTED OUTPUT
We can add the following code to the file:
${tripleTick[0]}typescript
\`\`\`
// existing code...
const subtractNumbers = (a, b) => a - b
const exponentiateNumbers = (a, b) => Math.pow(a, b)
const divideNumbers = (a, b) => a / b
const subtractNumbers = (a, b) => a - b;
const multiplyNumbers = (a, b) => a * b;
// existing code...
\`\`\`
const raiseAll = (vector: number[], power: number) => {
for (let i = 0; i < vector.length; i += 1)
vector[i] = exponentiateNumbers(vector[i], power)
return vector
}
${tripleTick[1]}
## EXAMPLE
## EXAMPLE 2
FILES
fib.ts
${tripleTick[0]}typescript
selected file \`fib.ts\`:
\`\`\`
const dfs = (root) => {
if (!root) return;
@@ -107,20 +63,21 @@ const fib = (n) => {
if (n < 1) return 1
return fib(n - 1) + fib(n - 2)
}
${tripleTick[1]}
\`\`\`
SELECTIONS
fib.ts (lines 10:10)
${tripleTick[0]}typescript
SELECTION
\`\`\`
return fib(n - 1) + fib(n - 2)
${tripleTick[1]}
\`\`\`
INSTRUCTIONS
\`\`\`
memoize results
\`\`\`
ACCEPTED OUTPUT
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:
${tripleTick[0]}typescript
\`\`\`
// existing code...
const fib = (n, memo = {}) => {
if (n < 1) return 1;
@@ -128,7 +85,7 @@ const fib = (n, memo = {}) => {
memo[n] = fib(n - 1, memo) + fib(n - 2, memo); // Store result in memo
return memo[n];
}
${tripleTick[1]}
\`\`\`
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.
@@ -138,172 +95,193 @@ Store Result: After computing fib(n), the result is stored in memo for future re
`
type FileSelnLocal = { fileURI: URI, content: string }
const stringifyFileSelection = ({ fileURI, content }: FileSelnLocal) => {
return `\
${fileURI.fsPath}
${tripleTick[0]}${filenameToVscodeLanguage(fileURI.fsPath) ?? ''}
${content}
${tripleTick[1]}
`
}
const stringifyCodeSelection = ({ fileURI, selectionStr, range }: CodeSelection) => {
return `\
${fileURI.fsPath} (lines ${range.startLineNumber}:${range.endLineNumber})
${tripleTick[0]}${filenameToVscodeLanguage(fileURI.fsPath) ?? ''}
${selectionStr}
${tripleTick[1]}
`
}
const failToReadStr = 'Could not read content. This file may have been deleted. If you expected content here, you can tell the user about this as they might not know.'
const stringifyFileSelections = async (fileSelections: FileSelection[], modelService: IModelService, fileService: IFileService) => {
if (fileSelections.length === 0) return null
const fileSlns: FileSelnLocal[] = await Promise.all(fileSelections.map(async (sel) => {
const content = await VSReadFile(modelService, fileService, sel.fileURI) ?? failToReadStr
return { ...sel, content }
}))
return fileSlns.map(sel => stringifyFileSelection(sel)).join('\n')
}
const stringifyCodeSelections = (codeSelections: CodeSelection[]) => {
return codeSelections.map(sel => stringifyCodeSelection(sel)).join('\n')
}
const stringifySelectionNames = (currSelns: StagingSelectionItem[] | null): string => {
if (!currSelns) return ''
return currSelns.map(s => `${s.fileURI.fsPath}${s.range ? ` (lines ${s.range.startLineNumber}:${s.range.endLineNumber})` : ''}`).join('\n')
}
export const chat_userMessageContent = async (instructions: string, prevSelns: StagingSelectionItem[] | null, currSelns: StagingSelectionItem[] | null) => {
const selnsStr = stringifySelectionNames(currSelns)
let str = ''
if (selnsStr) { str += `SELECTIONS\n${selnsStr}\n` }
str += `\nINSTRUCTIONS\n${instructions}`
return str;
};
export const chat_userMessageContentWithAllFilesToo = async (instructions: string, prevSelns: StagingSelectionItem[] | null, currSelns: StagingSelectionItem[] | null, modelService: IModelService, fileService: IFileService) => {
// ADD IN FILES AT TOP
const allSelections = [...currSelns || [], ...prevSelns || []]
const codeSelections: CodeSelection[] = []
const fileSelections: FileSelection[] = []
const filesURIs = new Set<string>()
for (const selection of allSelections) {
if (selection.type === 'Selection') {
codeSelections.push(selection)
}
else if (selection.type === 'File') {
const fileSelection = selection
const path = fileSelection.fileURI.fsPath
if (!filesURIs.has(path)) {
filesURIs.add(path)
fileSelections.push(fileSelection)
}
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`
}
const filesStr = await stringifyFileSelections(fileSelections, modelService, fileService)
const selnsStr = stringifyCodeSelections(codeSelections)
// ACTUAL MESSAGE CONTENT
const messageContent = await chat_userMessageContent(instructions, prevSelns, currSelns)
let str = ''
str += 'ALL FILE CONTENTS\n'
if (filesStr) str += `${filesStr}\n`
if (selnsStr) str += `${selnsStr}\n`
if (messageContent) str += `\n${messageContent}\n`
str += `${instructions}`;
return str;
};
export const fastApply_rewritewholething_systemMessage = `\
You are a coding assistant that re-writes an entire file to make a change. You are given the original file \`ORIGINAL_FILE\` and a change \`CHANGE\`.
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. Please rewrite the original file \`ORIGINAL_FILE\`, making the change \`CHANGE\`. You must completely re-write the whole file.
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. ONLY output the full new file. Do not add any other explanations or text.
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 fastApply_rewritewholething_userMessage = ({ originalCode, applyStr, uri }: { originalCode: string, applyStr: string, uri: URI }) => {
const language = filenameToVscodeLanguage(uri.fsPath) ?? ''
export const ctrlLStream_prompt = ({ originalCode, userMessage }: { originalCode: string, userMessage: string }) => {
return `\
ORIGINAL_FILE
${tripleTick[0]}${language}
ORIGINAL_CODE
\`\`\`
${originalCode}
${tripleTick[1]}
\`\`\`
CHANGE
${tripleTick[0]}
${applyStr}
${tripleTick[1]}
DIFF
\`\`\`
${userMessage}
\`\`\`
INSTRUCTIONS
Please finish writing the new file by applying the change to the original file. Return ONLY the completion of the file, without any explanation.
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 fastApply_searchreplace_systemMessage = `\
You are a coding assistant that re-writes an entire file to make a change. You are given the original file \`ORIGINAL_FILE\` and a change \`CHANGE\`.
Directions:
1. Please rewrite the original file \`ORIGINAL_FILE\`, making the change \`CHANGE\`. You must completely re-write the whole file.
2. Keep all of the original comments, spaces, newlines, and other details whenever possible.
3. ONLY output the full new file. Do not add any other explanations or text.
export const ctrlKStream_systemMessage = `\
`
export const fastApply_searchreplace_userMessage = ({ originalCode, applyStr, uri }: { originalCode: string, applyStr: string, uri: URI }) => {
const language = filenameToVscodeLanguage(uri.fsPath) ?? ''
return `\
ORIGINAL_FILE
\`\`\`${language}
${originalCode}
\`\`\`
CHANGE
\`\`\`
${applyStr}
\`\`\`
INSTRUCTIONS
Please finish writing the new file by applying the change to the original file. Return ONLY the completion of the file, without any explanation.
`
}
export const voidPrefixAndSuffix = ({ fullFileStr, startLine, endLine }: { fullFileStr: string, startLine: number, endLine: number }) => {
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_PREFIX_SUFFIX_CHARS = 20_000
const MAX_CHARS = 1024
/*
a
@@ -324,7 +302,7 @@ export const voidPrefixAndSuffix = ({ fullFileStr, startLine, endLine }: { fullF
// 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_PREFIX_SUFFIX_CHARS) { // +1 to include the \n
if (newLine.length + 1 + prefix.length <= MAX_CHARS) { // +1 to include the \n
prefix = `${newLine}\n${prefix}`
i -= 1
}
@@ -335,7 +313,7 @@ export const voidPrefixAndSuffix = ({ fullFileStr, startLine, endLine }: { fullF
let j = endLine - 1
while (j !== fullFileLines.length - 1) {
const newLine = fullFileLines[j + 1]
if (newLine.length + 1 + suffix.length <= MAX_PREFIX_SUFFIX_CHARS) { // +1 to include the \n
if (newLine.length + 1 + suffix.length <= MAX_CHARS) { // +1 to include the \n
suffix = `${suffix}\n${newLine}`
j += 1
}
@@ -346,59 +324,320 @@ export const voidPrefixAndSuffix = ({ fullFileStr, startLine, endLine }: { fullF
}
export const ctrlKStream_prompt = ({ selection, prefix, suffix, userMessage }: { selection: string, prefix: string, suffix: string, userMessage: string, }) => {
const onlySpeaksFIM = false
export type FimTagsType = {
preTag: string,
sufTag: string,
midTag: string
}
export const defaultFimTags: FimTagsType = {
preTag: 'ABOVE',
sufTag: 'BELOW',
midTag: 'SELECTION',
}
// this should probably be longer
export const ctrlKStream_systemMessage = ({ fimTags: { preTag, midTag, sufTag } }: { fimTags: FimTagsType }) => {
return `\
You are a FIM (fill-in-the-middle) coding assistant. Your task is to fill in the middle SELECTION marked by <${midTag}> tags.
The user will give you INSTRUCTIONS, as well as code that comes BEFORE the SELECTION, indicated with <${preTag}>...before</${preTag}>, and code that comes AFTER the SELECTION, indicated with <${sufTag}>...after</${sufTag}>.
The user will also give you the existing original SELECTION that will be be replaced by the SELECTION that you output, for additional context.
Instructions:
1. Your OUTPUT should be a SINGLE PIECE OF CODE of the form <${midTag}>...new_code</${midTag}>. Do NOT output any text or explanations before or after this.
2. You may ONLY CHANGE the original SELECTION, and NOT the content in the <${preTag}>...</${preTag}> or <${sufTag}>...</${sufTag}> tags.
3. Make sure all brackets in the new selection are balanced the same as in the original selection.
4. Be careful not to duplicate or remove variables, comments, or other syntax by mistake.
`
}
export const ctrlKStream_userMessage = ({ selection, prefix, suffix, instructions, fimTags, isOllamaFIM, language }: {
selection: string, prefix: string, suffix: string, instructions: string, fimTags: FimTagsType, language: string,
isOllamaFIM: false, // we require this be false for clarity
}) => {
const { preTag, sufTag, midTag } = fimTags
// prompt the model artifically on how to do FIM
// const preTag = 'BEFORE'
// const sufTag = 'AFTER'
// const midTag = 'SELECTION'
return `\
CURRENT SELECTION
${tripleTick[0]}${language}
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}>
${tripleTick[1]}
\`\`\`
INSTRUCTIONS
${instructions}
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}>
Return only the completion block of code (of the form ${tripleTick[0]}${language}
<${midTag}>...new code</${midTag}>
${tripleTick[1]}).`
<${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,26 +1,23 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
*-----------------------------------------------------------------------------------------*/
import { 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 { roundRangeToLines } from './sidebarActions.js';
import { VOID_CTRL_K_ACTION_ID } from './actionIDs.js';
import { localize2 } from '../../../../nls.js';
import { IMetricsService } from '../common/metricsService.js';
import { InputBox } from '../../../../base/browser/ui/inputbox/inputBox.js';
export type QuickEditPropsType = {
diffareaid: number,
initStreamingDiffZoneId: number | null,
textAreaRef: (ref: HTMLTextAreaElement | null) => void;
onGetInputBox: (i: InputBox) => void;
onChangeHeight: (height: number) => void;
onChangeText: (text: string) => void;
onUserUpdateText: (text: string) => void;
initText: string | null;
}
@@ -33,16 +30,16 @@ export type QuickEdit = {
}
export const VOID_CTRL_K_ACTION_ID = 'void.ctrlKAction'
registerAction2(class extends Action2 {
constructor(
) {
super({
id: VOID_CTRL_K_ACTION_ID,
f1: true,
title: localize2('voidQuickEditAction', 'Void: Quick Edit'),
title: 'Void: Quick Edit',
keybinding: {
primary: KeyMod.CtrlCmd | KeyCode.KeyK,
weight: KeybindingWeight.VoidExtension,
weight: KeybindingWeight.BuiltinExtension,
}
});
}
@@ -51,18 +48,21 @@ registerAction2(class extends Action2 {
const editorService = accessor.get(ICodeEditorService)
const metricsService = accessor.get(IMetricsService)
metricsService.capture('Ctrl+K', {})
metricsService.capture('User Action', { type: 'Open Ctrl+K' })
const editor = editorService.getActiveCodeEditor()
if (!editor) return;
const model = editor.getModel()
if (!model) return;
const selection = roundRangeToLines(editor.getSelection(), { emptySelectionBehavior: 'line' })
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 })
}
@@ -1,7 +1,7 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
*-----------------------------------------------------------------------------------------*/
import { Emitter, Event } from '../../../../base/common/event.js';
import { Disposable } from '../../../../base/common/lifecycle.js';
@@ -1,7 +1,7 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
*-----------------------------------------------------------------------------------------*/
import { spawn, execSync } from 'child_process';
// Added lines below
@@ -11,91 +11,34 @@ import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
function doesPathExist(filePath) {
try {
const stats = fs.statSync(filePath);
return stats.isFile();
} catch (err) {
if (err.code === 'ENOENT') {
return false;
}
throw err;
}
}
/*
This function finds `globalDesiredPath` given `localDesiredPath` and `currentPath`
Diagram:
...basePath/
└── void/
├── ...currentPath/ (defined globally)
└── ...localDesiredPath/ (defined locally)
*/
function findDesiredPathFromLocalPath(localDesiredPath, currentPath) {
// walk upwards until currentPath + localDesiredPath exists
while (!doesPathExist(path.join(currentPath, localDesiredPath))) {
const parentDir = path.dirname(currentPath);
if (parentDir === currentPath) {
return undefined;
}
currentPath = parentDir;
}
// return the `globallyDesiredPath`
const globalDesiredPath = path.join(currentPath, localDesiredPath)
return globalDesiredPath;
}
const __void_name = 'void'
// 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;
}
// Find "void" in __dirname and use that as our base:
const voidIdx = __dirname.indexOf(__void_name);
const baseDir = __dirname.substring(0, voidIdx + __void_name.length);
const target = path.join(
baseDir,
'src/vs/workbench/contrib/void/browser/react/src2/styles.css'
);
// Or re-write with the same content:
const content = fs.readFileSync(pathToCssFile, 'utf8');
fs.writeFileSync(pathToCssFile, content, 'utf8');
const content = fs.readFileSync(target, 'utf8');
fs.writeFileSync(target, content, 'utf8');
console.log('[scope-tailwind] Force-saved styles.css');
} catch (err) {
console.error('[scope-tailwind] Error saving styles.css:', err);
}
}, 3000);
}, 5000);
}
const args = process.argv.slice(2);
const isWatch = args.includes('--watch') || args.includes('-w');
if (isWatch) {
// this just builds it if it doesn't exist instead of waiting for the watcher to trigger
// Check if src2/ exists; if not, do an initial scope-tailwind build
if (!fs.existsSync('src2')) {
try {
console.log('🔨 Running initial scope-tailwind build to create src2 folder...');
execSync(
'npx scope-tailwind ./src -o src2/ -s void-scope -c styles.css -p "void-"',
{ stdio: 'inherit' }
);
console.log('✅ src2/ created successfully.');
} catch (err) {
console.error('❌ Error running initial scope-tailwind build:', err);
process.exit(1);
}
}
// Watch mode
const scopeTailwindWatcher = spawn('npx', [
'nodemon',
@@ -1,21 +1,21 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
*-----------------------------------------------------------------------------------------*/
import React, { useEffect, useState } from 'react'
import { useEffect, useState } from 'react'
import { useIsDark, useSidebarState } from '../util/services.js'
import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js'
import { QuickEditChat } from './QuickEditChat.js'
import { CtrlKChat } from './CtrlKChat.js'
import { QuickEditPropsType } from '../../../quickEditActions.js'
export const QuickEdit = (props: QuickEditPropsType) => {
export const CtrlK = (props: QuickEditPropsType) => {
const isDark = useIsDark()
return <div className={`@@void-scope ${isDark ? 'dark' : ''}`}>
<ErrorBoundary>
<QuickEditChat {...props} />
<CtrlKChat {...props} />
</ErrorBoundary>
</div>
@@ -0,0 +1,177 @@
/*------------------------------------------------------------------------------------------
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
*-----------------------------------------------------------------------------------------*/
import React, { FormEvent, useCallback, useEffect, useRef, useState } from 'react';
import { useSettingsState, useSidebarState, useThreadsState, useQuickEditState, useAccessor } from '../util/services.js';
import { OnError } from '../../../../../../../platform/void/common/llmMessageTypes.js';
import { InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js';
import { getCmdKey } from '../../../helpers/getCmdKey.js';
import { VoidInputBox } from '../util/inputs.js';
import { QuickEditPropsType } from '../../../quickEditActions.js';
import { ButtonStop, ButtonSubmit } from '../sidebar-tsx/SidebarChat.js';
import { ModelDropdown } from '../void-settings-tsx/ModelDropdown.js';
import { X } from 'lucide-react';
export const CtrlKChat = ({ diffareaid, onGetInputBox, onUserUpdateText, onChangeHeight, initText }: QuickEditPropsType) => {
const accessor = useAccessor()
const inlineDiffsService = accessor.get('IInlineDiffsService')
const sizerRef = useRef<HTMLDivElement | null>(null)
const inputBoxRef: React.MutableRefObject<InputBox | null> = useRef(null);
useEffect(() => {
const inputContainer = sizerRef.current
if (!inputContainer) return;
// only observing 1 element
let resizeObserver: ResizeObserver | undefined
resizeObserver = new ResizeObserver((entries) => {
const height = entries[0].borderBoxSize[0].blockSize
onChangeHeight(height)
})
resizeObserver.observe(inputContainer);
return () => { resizeObserver?.disconnect(); };
}, [onChangeHeight]);
// state of current message
const [instructions, setInstructions] = useState(initText ?? '') // the user's instructions
const onChangeText = useCallback((newStr: string) => {
setInstructions(newStr)
onUserUpdateText(newStr)
}, [setInstructions])
const isDisabled = !instructions.trim()
const currentlyStreamingIdRef = useRef<number | undefined>(undefined)
const [isStreaming, setIsStreaming] = useState(false)
const onSubmit = useCallback((e: FormEvent) => {
if (currentlyStreamingIdRef.current !== undefined) return
inputBoxRef.current?.disable()
currentlyStreamingIdRef.current = inlineDiffsService.startApplying({
featureName: 'Ctrl+K',
diffareaid: diffareaid,
userMessage: instructions,
})
setIsStreaming(true)
}, [inlineDiffsService, diffareaid, instructions])
const onInterrupt = useCallback(() => {
if (currentlyStreamingIdRef.current !== undefined)
inlineDiffsService.interruptStreaming(currentlyStreamingIdRef.current)
inputBoxRef.current?.enable()
setIsStreaming(false)
}, [inlineDiffsService])
// sync init value
const alreadySetRef = useRef(false)
useEffect(() => {
if (!inputBoxRef.current) return
if (alreadySetRef.current) return
alreadySetRef.current = true
inputBoxRef.current.value = instructions
}, [initText, instructions])
return <div ref={sizerRef} className='py-2 w-full max-w-xl'>
<form
// copied from SidebarChat.tsx
className={`
flex flex-col gap-2 py-1 px-2 relative input text-left shrink-0
transition-all duration-200
rounded-md
bg-vscode-input-bg
border border-vscode-commandcenter-inactive-border focus-within:border-vscode-commandcenter-active-border hover:border-vscode-commandcenter-active-border
`
}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
onSubmit(e)
return
}
}}
onSubmit={(e) => {
if (isDisabled) {
// __TODO__ show disabled
return
}
console.log('submit!')
onSubmit(e)
}}
onClick={(e) => {
inputBoxRef.current?.focus()
}}
>
{/* // this div is used to position the input box properly */}
<div
className={`w-full z-[999] relative
@@[&_textarea]:!void-bg-transparent
@@[&_textarea]:!void-outline-none
@@[&_textarea]:!void-text-vscode-input-fg
@@[&_div.monaco-inputbox]:!void-border-none
@@[&_div.monaco-inputbox]:!void-outline-none`}
>
<div className='flex flex-row justify-between items-end gap-1'>
<div className='absolute size-0.5 top-0 right-4 z-[1]'>
<X
onClick={() => { inlineDiffsService.removeCtrlKZone({ diffareaid }) }}
/>
</div>
{/* input */}
<div // copied from SidebarChat.tsx
className={`w-full
@@[&_textarea]:!void-bg-transparent @@[&_textarea]:!void-outline-none @@[&_textarea]:!void-text-vscode-input-fg @@[&_div.monaco-inputbox]:!void-outline-none`}>
{/* text input */}
<VoidInputBox
placeholder={`${getCmdKey()}+K to select`}
onChangeText={onChangeText}
onCreateInstance={useCallback((instance: InputBox) => {
inputBoxRef.current = instance;
onGetInputBox(instance);
instance.focus()
}, [onGetInputBox])}
multiline={true}
/>
</div>
</div>
{/* bottom row */}
<div
className='flex flex-row justify-between items-end gap-1'
>
{/* submit options */}
<div className='max-w-[150px]
@@[&_select]:!void-border-none
@@[&_select]:!void-outline-none'
>
<ModelDropdown featureName='Ctrl+K' />
</div>
{/* submit / stop button */}
{isStreaming ?
// stop button
<ButtonStop
onClick={onInterrupt}
/>
:
// submit button (up arrow)
<ButtonSubmit
disabled={isDisabled}
/>
}
</div>
</div>
</form>
</div>
}
@@ -0,0 +1,12 @@
/*------------------------------------------------------------------------------------------
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
*-----------------------------------------------------------------------------------------*/
import { mountFnGenerator } from '../util/mountFnGenerator.js'
import { CtrlK } from './CtrlK.js'
export const mountCtrlK = mountFnGenerator(CtrlK)
@@ -1,7 +1,7 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
*-----------------------------------------------------------------------------------------*/
import { diffLines, Change } from 'diff';
@@ -1,29 +1,87 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
*-----------------------------------------------------------------------------------------*/
import React from 'react';
import { VoidCodeEditor, VoidCodeEditorProps } from '../util/inputs.js';
import { ReactNode } from "react"
import { VoidCodeEditor } from '../util/inputs.js';
export const BlockCode = ({ buttonsOnHover, ...codeEditorProps }: { buttonsOnHover?: React.ReactNode } & VoidCodeEditorProps) => {
const isSingleLine = !codeEditorProps.initValue.includes('\n')
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',
return (
<>
<div className="relative group w-full overflow-hidden my-4">
{buttonsOnHover === null ? null : (
<div className={`z-[1] absolute top-0 right-0 opacity-0 group-hover:opacity-100 duration-200 ${isSingleLine ? 'h-full flex items-center' : ''}`}>
<div className={`flex space-x-1 ${isSingleLine ? 'pr-2' : 'p-2'}`}>
{buttonsOnHover}
</div>
</div>
)}
// 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',
<VoidCodeEditor {...codeEditorProps} />
</div>
</>
// Markup/Config
'md': 'markdown',
'markdown': 'markdown',
'xml': 'xml',
'svg': 'xml',
'yaml': 'yaml',
'yml': 'yaml',
'ini': 'ini',
'toml': 'ini',
// Other
'sql': 'sql',
'graphql': 'graphql',
'gql': 'graphql',
'dockerfile': 'dockerfile',
'docker': 'dockerfile'
};
export function getLanguageFromFileName(fileName: string): string {
const ext = fileName.toLowerCase().split('.').pop();
if (!ext) return 'plaintext';
return extensionMap[ext] || 'plaintext';
}
export const BlockCode = ({ text, buttonsOnHover, language }: { text: string, buttonsOnHover?: ReactNode, language?: string }) => {
const isSingleLine = !text.includes('\n')
return (<>
<div className={`relative group w-full bg-vscode-editor-bg overflow-hidden isolate`}>
{buttonsOnHover === null ? null : (
<div className="z-[1] absolute top-0 right-0 opacity-0 group-hover:opacity-100 duration-200">
<div className={`flex space-x-2 ${isSingleLine ? '' : 'p-2'}`}>{buttonsOnHover}</div>
</div>
)}
<VoidCodeEditor
initValue={text}
language={language}
/>
</div>
</>
)
}
@@ -1,14 +1,12 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
*-----------------------------------------------------------------------------------------*/
import React, { JSX, useCallback, useEffect, useState } from 'react'
import { marked, MarkedToken, Token } from 'marked'
import { BlockCode } from './BlockCode.js'
import { useAccessor, useChatThreadsState, useChatThreadsStreamState } from '../util/services.js'
import { ChatLocation, getApplyBoxId, } from '../../../searchAndReplaceService.js'
import { nameToVscodeLanguage } from '../../../helpers/detectLanguage.js'
import { useAccessor } from '../util/services.js'
enum CopyButtonState {
@@ -19,14 +17,12 @@ enum CopyButtonState {
const COPY_FEEDBACK_TIMEOUT = 1000 // amount of time to say 'Copied!'
const ApplyButtonsOnHover = ({ applyStr, applyBoxId }: { applyStr: string, applyBoxId: string }) => {
const CodeButtonsOnHover = ({ text }: { text: string }) => {
const accessor = useAccessor()
const [copyButtonState, setCopyButtonState] = useState(CopyButtonState.Copy)
const inlineDiffService = accessor.get('IInlineDiffsService')
const clipboardService = accessor.get('IClipboardService')
const metricsService = accessor.get('IMetricsService')
useEffect(() => {
if (copyButtonState !== CopyButtonState.Copy) {
@@ -37,35 +33,30 @@ const ApplyButtonsOnHover = ({ applyStr, applyBoxId }: { applyStr: string, apply
}, [copyButtonState])
const onCopy = useCallback(() => {
clipboardService.writeText(applyStr)
clipboardService.writeText(text)
.then(() => { setCopyButtonState(CopyButtonState.Copied) })
.catch(() => { setCopyButtonState(CopyButtonState.Error) })
metricsService.capture('Copy Code', { length: applyStr.length }) // capture the length only
}, [metricsService, clipboardService, applyStr])
}, [text, clipboardService])
const onApply = useCallback(() => {
inlineDiffService.startApplying({
from: 'Chat',
applyStr,
applyBoxId,
featureName: 'Ctrl+L',
userMessage: text,
})
metricsService.capture('Apply Code', { length: applyStr.length }) // capture the length only
}, [metricsService, inlineDiffService, applyStr])
}, [inlineDiffService])
const isSingleLine = !applyStr.includes('\n')
const isSingleLine = !text.includes('\n')
return <>
<button
className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-1 text-void-fg-1 hover:brightness-110 border border-vscode-input-border rounded`}
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
// btn btn-secondary btn-sm border text-sm border-vscode-input-border rounded
className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-1 text-void-fg-1 hover:brightness-110 border border-vscode-input-border rounded`}
// 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
@@ -73,22 +64,8 @@ const ApplyButtonsOnHover = ({ applyStr, applyBoxId }: { applyStr: string, apply
</>
}
export const CodeSpan = ({ children, className }: { children: React.ReactNode, className?: string }) => {
return <code className={`
bg-void-bg-1
px-1
rounded-sm
font-mono font-medium
break-all
${className}
`}
>
{children}
</code>
}
const RenderToken = ({ token, nested = false, noSpace = false, chatLocation, tokenId = '', }: { token: Token | string, nested?: boolean, noSpace?: boolean, chatLocation?: ChatLocation, tokenId?: string, }): JSX.Element => {
const RenderToken = ({ token, nested = false }: { token: Token | string, nested?: boolean }): JSX.Element => {
// deal with built-in tokens first (assume marked token)
const t = token as MarkedToken
@@ -98,77 +75,54 @@ const RenderToken = ({ token, nested = false, noSpace = false, chatLocation, tok
}
if (t.type === "code") {
const isCodeblockClosed = t.raw?.startsWith('```') && t.raw?.endsWith('```');
const applyBoxId = getApplyBoxId({
threadId: chatLocation!.threadId,
messageIdx: chatLocation!.messageIdx,
codeblockId: tokenId,
})
return <BlockCode
initValue={t.text}
language={t.lang === undefined ? undefined : nameToVscodeLanguage[t.lang]}
buttonsOnHover={<ApplyButtonsOnHover applyStr={t.text} applyBoxId={applyBoxId} />}
text={t.text}
// language={t.lang} // instead use vscode to detect language
buttonsOnHover={<CodeButtonsOnHover text={t.text} />}
/>
}
if (t.type === "heading") {
const HeadingTag = `h${t.depth}` as keyof JSX.IntrinsicElements
const headingClasses: { [h: string]: string } = {
h1: "text-4xl font-semibold mt-6 mb-4 pb-2 border-b border-void-bg-2",
h2: "text-3xl font-semibold mt-6 mb-4 pb-2 border-b border-void-bg-2",
h3: "text-2xl font-semibold mt-6 mb-4",
h4: "text-xl font-semibold mt-6 mb-4",
h5: "text-lg font-semibold mt-6 mb-4",
h6: "text-base font-semibold mt-6 mb-4 text-gray-600"
}
return <HeadingTag className={headingClasses[HeadingTag]}>{t.text}</HeadingTag>
return <HeadingTag>{t.text}</HeadingTag>
}
if (t.type === "table") {
return (
<div className={`${noSpace ? '' : 'my-4'} overflow-x-auto`}>
<table className="min-w-full border border-void-bg-2">
<thead>
<tr className="bg-void-bg-1">
{t.header.map((cell: any, index: number) => (
<th
key={index}
className="px-4 py-2 border border-void-bg-2 font-semibold"
style={{ textAlign: t.align[index] || "left" }}
<table>
<thead>
<tr>
{t.header.map((cell: any, index: number) => (
<th key={index} style={{ textAlign: t.align[index] || "left" }}>
{cell.raw}
</th>
))}
</tr>
</thead>
<tbody>
{t.rows.map((row: any[], rowIndex: number) => (
<tr key={rowIndex}>
{row.map((cell: any, cellIndex: number) => (
<td
key={cellIndex}
style={{ textAlign: t.align[cellIndex] || "left" }}
>
{cell.raw}
</th>
</td>
))}
</tr>
</thead>
<tbody>
{t.rows.map((row: any[], rowIndex: number) => (
<tr key={rowIndex} className={rowIndex % 2 === 0 ? 'bg-white' : 'bg-void-bg-1'}>
{row.map((cell: any, cellIndex: number) => (
<td
key={cellIndex}
className="px-4 py-2 border border-void-bg-2"
style={{ textAlign: t.align[cellIndex] || "left" }}
>
{cell.raw}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
))}
</tbody>
</table>
)
}
if (t.type === "hr") {
return <hr className="my-6 border-t border-void-bg-2" />
return <hr />
}
if (t.type === "blockquote") {
return <blockquote className={`pl-4 border-l-4 border-void-bg-2 italic ${noSpace ? '' : 'my-4'}`}>{t.text}</blockquote>
return <blockquote>{t.text}</blockquote>
}
if (t.type === "list") {
@@ -176,16 +130,14 @@ const RenderToken = ({ token, nested = false, noSpace = false, chatLocation, tok
return (
<ListTag
start={t.start ? t.start : undefined}
className={`list-inside pl-2 ${noSpace ? '' : 'my-4'} ${t.ordered ? "list-decimal" : "list-disc"}`}
className={`list-inside ${t.ordered ? "list-decimal" : "list-disc"}`}
>
{t.items.map((item, index) => (
<li key={index} className={`${noSpace ? '' : 'mb-4'}`}>
<li key={index}>
{item.task && (
<input type="checkbox" checked={item.checked} readOnly className="mr-2 form-checkbox" />
<input type="checkbox" checked={item.checked} readOnly />
)}
<span className="ml-1">
<ChatMarkdownRender string={item.text} nested={true} />
</span>
<ChatMarkdownRender string={item.text} nested={true} />
</li>
))}
</ListTag>
@@ -195,21 +147,22 @@ const RenderToken = ({ token, nested = false, noSpace = false, chatLocation, tok
if (t.type === "paragraph") {
const contents = <>
{t.tokens.map((token, index) => (
<RenderToken key={index} token={token} tokenId={`${tokenId}-${index}`} /> // assign a unique tokenId to nested components
<RenderToken key={index} token={token} />
))}
</>
if (nested) return contents
return <p className={`${noSpace ? '' : 'my-4'}`}>
{contents}
</p>
if (nested)
return contents
return <p>{contents}</p>
}
// don't actually render <html> tags, just render strings of them
if (t.type === "html") {
return (
<p className={`${noSpace ? '' : 'my-4'}`}>
<pre>
{`<html>`}
{t.raw}
</p>
{`</html>`}
</pre>
)
}
@@ -223,40 +176,30 @@ const RenderToken = ({ token, nested = false, noSpace = false, chatLocation, tok
if (t.type === "link") {
return (
<a
className='underline'
onClick={() => { window.open(t.href) }}
href={t.href}
title={t.title ?? undefined}
>
<a className='underline' onClick={() => { window.open(t.href) }} href={t.href} title={t.title ?? undefined}>
{t.text}
</a>
)
}
if (t.type === "image") {
return <img
src={t.href}
alt={t.text}
title={t.title ?? undefined}
className={`max4w-full h-auto rounded ${noSpace ? '' : 'my-4'}`}
/>
return <img src={t.href} alt={t.text} title={t.title ?? undefined} />
}
if (t.type === "strong") {
return <strong className="font-semibold">{t.text}</strong>
return <strong>{t.text}</strong>
}
if (t.type === "em") {
return <em className="italic">{t.text}</em>
return <em>{t.text}</em>
}
// inline code
if (t.type === "codespan") {
return (
<CodeSpan>
<code className="text-vscode-text-preformat-fg bg-vscode-text-preformat-bg px-1 rounded-sm font-mono">
{t.text}
</CodeSpan>
</code>
)
}
@@ -266,24 +209,24 @@ const RenderToken = ({ token, nested = false, noSpace = false, chatLocation, tok
// strikethrough
if (t.type === "del") {
return <del className="line-through">{t.text}</del>
return <del>{t.text}</del>
}
// default
return (
<div className="bg-orange-50 rounded-sm overflow-hidden p-2">
<span className="text-sm text-orange-500">Unknown type:</span>
<div className="bg-orange-50 rounded-sm overflow-hidden">
<span className="text-xs text-orange-500">Unknown type:</span>
{t.raw}
</div>
)
}
export const ChatMarkdownRender = ({ string, nested = false, noSpace, chatLocation }: { string: string, nested?: boolean, noSpace?: boolean, chatLocation?: ChatLocation }) => {
export const ChatMarkdownRender = ({ string, nested = false }: { string: string, nested?: boolean }) => {
const tokens = marked.lexer(string); // https://marked.js.org/using_pro#renderer
return (
<>
{tokens.map((token, index) => (
<RenderToken key={index} token={token} nested={nested} noSpace={noSpace} chatLocation={chatLocation} />
<RenderToken key={index} token={token} nested={nested} />
))}
</>
)
@@ -1,126 +0,0 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import React, { FormEvent, useCallback, useEffect, useRef, useState } from 'react';
import { useSettingsState, useSidebarState, useChatThreadsState, useQuickEditState, useAccessor } from '../util/services.js';
import { TextAreaFns, VoidInputBox2 } from '../util/inputs.js';
import { QuickEditPropsType } from '../../../quickEditActions.js';
import { ButtonStop, ButtonSubmit, IconX, VoidChatArea } from '../sidebar-tsx/SidebarChat.js';
import { ModelDropdown } from '../void-settings-tsx/ModelDropdown.js';
import { VOID_CTRL_K_ACTION_ID } from '../../../actionIDs.js';
import { useRefState } from '../util/helpers.js';
import { useScrollbarStyles } from '../util/useScrollbarStyles.js';
import { isFeatureNameDisabled } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js';
export const QuickEditChat = ({
diffareaid,
initStreamingDiffZoneId,
onChangeHeight,
onChangeText: onChangeText_,
textAreaRef: textAreaRef_,
initText
}: QuickEditPropsType) => {
const accessor = useAccessor()
const inlineDiffsService = accessor.get('IInlineDiffsService')
const sizerRef = useRef<HTMLDivElement | null>(null)
const textAreaRef = useRef<HTMLTextAreaElement | null>(null)
const textAreaFnsRef = useRef<TextAreaFns | null>(null)
useEffect(() => {
const inputContainer = sizerRef.current
if (!inputContainer) return;
// only observing 1 element
let resizeObserver: ResizeObserver | undefined
resizeObserver = new ResizeObserver((entries) => {
const height = entries[0].borderBoxSize[0].blockSize
onChangeHeight(height)
})
resizeObserver.observe(inputContainer);
return () => { resizeObserver?.disconnect(); };
}, [onChangeHeight]);
const settingsState = useSettingsState()
// state of current message
const [instructionsAreEmpty, setInstructionsAreEmpty] = useState(!(initText ?? '')) // the user's instructions
const isDisabled = instructionsAreEmpty || !!isFeatureNameDisabled('Ctrl+K', settingsState)
const [currStreamingDiffZoneRef, setCurrentlyStreamingDiffZone] = useRefState<number | null>(initStreamingDiffZoneId)
const isStreaming = currStreamingDiffZoneRef.current !== null
const onSubmit = useCallback(() => {
if (isDisabled) return
if (currStreamingDiffZoneRef.current !== null) return
textAreaFnsRef.current?.disable()
const id = inlineDiffsService.startApplying({
from: 'QuickEdit',
diffareaid: diffareaid,
})
setCurrentlyStreamingDiffZone(id ?? null)
}, [currStreamingDiffZoneRef, setCurrentlyStreamingDiffZone, isDisabled, inlineDiffsService, diffareaid])
const onInterrupt = useCallback(() => {
if (currStreamingDiffZoneRef.current === null) return
inlineDiffsService.interruptStreaming(currStreamingDiffZoneRef.current)
setCurrentlyStreamingDiffZone(null)
textAreaFnsRef.current?.enable()
}, [currStreamingDiffZoneRef, setCurrentlyStreamingDiffZone, inlineDiffsService])
const onX = useCallback(() => {
onInterrupt()
inlineDiffsService.removeCtrlKZone({ diffareaid })
}, [inlineDiffsService, diffareaid])
useScrollbarStyles(sizerRef)
const keybindingString = accessor.get('IKeybindingService').lookupKeybinding(VOID_CTRL_K_ACTION_ID)?.getLabel()
const chatAreaRef = useRef<HTMLDivElement | null>(null)
return <div ref={sizerRef} style={{ maxWidth: 450 }} className={`py-2 w-full`}>
<VoidChatArea
divRef={chatAreaRef}
onSubmit={onSubmit}
onAbort={onInterrupt}
onClose={onX}
isStreaming={isStreaming}
isDisabled={isDisabled}
featureName="Ctrl+K"
className="py-2 w-full"
onClickAnywhere={() => { textAreaRef.current?.focus() }}
>
<VoidInputBox2
className='px-1'
initValue={initText}
ref={useCallback((r: HTMLTextAreaElement | null) => {
textAreaRef.current = r
textAreaRef_(r)
r?.addEventListener('keydown', (e) => {
if (e.key === 'Escape')
onX()
})
}, [textAreaRef_, onX])}
fnsRef={textAreaFnsRef}
placeholder="Enter instructions..."
onChangeText={useCallback((newStr: string) => {
setInstructionsAreEmpty(!newStr)
onChangeText_(newStr)
}, [onChangeText_])}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
onSubmit()
return
}
}}
multiline={true}
/>
</VoidChatArea>
</div>
}
@@ -1,12 +0,0 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { mountFnGenerator } from '../util/mountFnGenerator.js'
import { QuickEdit } from './QuickEdit.js'
export const mountCtrlK = mountFnGenerator(QuickEdit)
@@ -1,7 +1,7 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
*-----------------------------------------------------------------------------------------*/
import React, { Component, ErrorInfo, ReactNode } from 'react';
import { ErrorDisplay } from './ErrorDisplay.js';
@@ -1,16 +1,14 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
*-----------------------------------------------------------------------------------------*/
import React, { useEffect, useState } from 'react';
import React, { useState } from 'react';
import { AlertCircle, ChevronDown, ChevronUp, X } from 'lucide-react';
import { errorDetails } from '../../../../../../../workbench/contrib/void/common/llmMessageTypes.js';
import { useSettingsState } from '../util/services.js';
export const ErrorDisplay = ({
message: message_,
message,
fullError,
onDismiss,
showDismiss,
@@ -22,46 +20,54 @@ export const ErrorDisplay = ({
}) => {
const [isExpanded, setIsExpanded] = useState(false);
const details = errorDetails(fullError)
const isExpandable = !!details
let details: string | null = null;
if (fullError === null) {
details = null
}
else if (typeof fullError === 'object') {
details = JSON.stringify(fullError, null, 2)
}
else if (typeof fullError === 'string') {
details = null
}
const message = message_ + ''
return (
<div className={`rounded-lg border border-red-200 bg-red-50 p-4 overflow-auto`}>
{/* Header */}
<div className='flex items-start justify-between'>
<div className='flex gap-3'>
<AlertCircle className='h-5 w-5 text-red-600 mt-0.5' />
<div className='flex-1'>
<h3 className='font-semibold text-red-800'>
<div className="flex items-start justify-between">
<div className="flex gap-3">
<AlertCircle className="h-5 w-5 text-red-600 mt-0.5" />
<div className="flex-1">
<h3 className="font-semibold text-red-800">
{/* eg Error */}
Error
</h3>
<p className='text-red-700 mt-1'>
<p className="text-red-700 mt-1">
{/* eg Something went wrong */}
{message}
</p>
</div>
</div>
<div className='flex gap-2'>
{isExpandable && (
<button className='text-red-600 hover:text-red-800 p-1 rounded'
<div className="flex gap-2">
{details && (
<button className="text-red-600 hover:text-red-800 p-1 rounded"
onClick={() => setIsExpanded(!isExpanded)}
>
{isExpanded ? (
<ChevronUp className='h-5 w-5' />
<ChevronUp className="h-5 w-5" />
) : (
<ChevronDown className='h-5 w-5' />
<ChevronDown className="h-5 w-5" />
)}
</button>
)}
{showDismiss && onDismiss && (
<button className='text-red-600 hover:text-red-800 p-1 rounded'
<button className="text-red-600 hover:text-red-800 p-1 rounded"
onClick={onDismiss}
>
<X className='h-5 w-5' />
<X className="h-5 w-5" />
</button>
)}
</div>
@@ -69,10 +75,10 @@ export const ErrorDisplay = ({
{/* Expandable Details */}
{isExpanded && details && (
<div className='mt-4 space-y-3 border-t border-red-200 pt-3 overflow-auto'>
<div className="mt-4 space-y-3 border-t border-red-200 pt-3 overflow-auto">
<div>
<span className='font-semibold text-red-800'>Full Error: </span>
<pre className='text-red-700'>{details}</pre>
<span className="font-semibold text-red-800">Full Error: </span>
<pre className="text-red-700">{details}</pre>
</div>
</div>
)}
@@ -1,7 +1,7 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
*-----------------------------------------------------------------------------------------*/
import React, { useEffect, useState } from 'react'
import { mountFnGenerator } from '../util/mountFnGenerator.js'
@@ -13,26 +13,18 @@ import { useIsDark, useSidebarState } from '../util/services.js';
// import { SidebarChat } from './SidebarChat.js';
import '../styles.css'
import { SidebarThreadSelector } from './SidebarThreadSelector.js';
import { SidebarChat } from './SidebarChat.js';
import ErrorBoundary from './ErrorBoundary.js';
export const Sidebar = ({ className }: { className: string }) => {
const sidebarState = useSidebarState()
const { currentTab: tab } = sidebarState
const { isHistoryOpen, currentTab: tab } = sidebarState
// const isDark = useIsDark()
return <div
className={`@@void-scope`} // ${isDark ? 'dark' : ''}
style={{ width: '100%', height: '100%' }}
>
<div
// default background + text styles for sidebar
className={`
w-full h-full
bg-void-bg-2
text-void-fg-1
`}
>
const isDark = useIsDark()
// ${isDark ? 'dark' : ''}
return <div className={`@@void-scope`} style={{ width: '100%', height: '100%' }}>
<div className={`w-full h-full flex flex-col py-2 bg-vscode-sidebar-bg`}>
{/* <span onClick={() => {
const tabs = ['chat', 'settings', 'threadSelector']
@@ -40,11 +32,11 @@ export const Sidebar = ({ className }: { className: string }) => {
sidebarStateService.setState({ currentTab: tabs[(index + 1) % tabs.length] as any })
}}>clickme {tab}</span> */}
{/* <div className={`w-full h-auto mb-2 ${isHistoryOpen ? '' : 'hidden'} ring-2 ring-widget-shadow z-10`}>
<div className={`w-full h-auto mb-2 ${isHistoryOpen ? '' : 'hidden'} ring-2 ring-widget-shadow z-10`}>
<ErrorBoundary>
<SidebarThreadSelector />
</ErrorBoundary>
</div> */}
</div>
<div className={`w-full h-full ${tab === 'chat' ? '' : 'hidden'}`}>
<ErrorBoundary>
File diff suppressed because it is too large Load Diff
@@ -1,12 +1,12 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
*-----------------------------------------------------------------------------------------*/
import React from "react";
import { useAccessor, useChatThreadsState } from '../util/services.js';
import { useAccessor, useThreadsState } from '../util/services.js';
import { IThreadHistoryService } from '../../../threadHistoryService.js';
import { ISidebarStateService } from '../../../sidebarStateService.js';
import { IconX } from './SidebarChat.js';
const truncate = (s: string) => {
@@ -19,102 +19,75 @@ const truncate = (s: string) => {
export const SidebarThreadSelector = () => {
const threadsState = useChatThreadsState()
const threadsState = useThreadsState()
const accessor = useAccessor()
const chatThreadsService = accessor.get('IChatThreadService')
const threadsStateService = accessor.get('IThreadHistoryService')
const sidebarStateService = accessor.get('ISidebarStateService')
const { allThreads } = threadsState
// sorted by most recent to least recent
const sortedThreadIds = Object.keys(allThreads ?? {})
.sort((threadId1, threadId2) => allThreads![threadId1].lastModified > allThreads![threadId2].lastModified ? -1 : 1)
.filter(threadId => allThreads![threadId].messages.length !== 0)
const sortedThreadIds = Object.keys(allThreads ?? {}).sort((threadId1, threadId2) => allThreads![threadId1].lastModified > allThreads![threadId2].lastModified ? -1 : 1)
return (
<div className="flex p-2 flex-col gap-y-1 max-h-[400px] overflow-y-auto">
<div className="flex flex-col gap-y-1 max-h-[400px] overflow-y-auto">
<div className="w-full relative flex justify-center items-center">
{/* title */}
<h2 className='font-bold text-lg'>{`History`}</h2>
{/* X button at top right */}
<button
type='button'
className='absolute top-0 right-0'
onClick={() => sidebarStateService.setState({ isHistoryOpen: false })}
>
<IconX
size={16}
className="p-[1px] stroke-[2] opacity-80 text-void-fg-3 hover:brightness-95"
/>
{/* X button at top right */}
<div className="text-right">
<button className="btn btn-sm" onClick={() => sidebarStateService.setState({ isHistoryOpen: false })}>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
className="size-4"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M6 18 18 6M6 6l12 12"
/>
</svg>
</button>
</div>
{/* a list of all the past threads */}
<div className="px-1">
<ul className="flex flex-col gap-y-0.5 overflow-y-auto list-disc">
<div className='flex flex-col gap-y-1 overflow-y-auto'>
{sortedThreadIds.map((threadId) => {
if (!allThreads)
return <>Error: Threads not found.</>
const pastThread = allThreads[threadId]
{sortedThreadIds.length === 0
let btnStringArr: string[] = []
? <div key="nothreads" className="text-center text-void-fg-3 brightness-90 text-sm">{`There are no chat threads yet.`}</div>
const firstMsgIdx = allThreads[threadId].messages.findIndex(msg => msg.role !== 'system' && !!msg.displayContent) ?? ''
if (firstMsgIdx !== -1)
btnStringArr.push(truncate(allThreads[threadId].messages[firstMsgIdx].displayContent ?? ''))
else
btnStringArr.push('""')
: sortedThreadIds.map((threadId) => {
if (!allThreads) {
return <li key="error" className="text-void-warning">{`Error accessing chat history.`}</li>;
}
const secondMsgIdx = allThreads[threadId].messages.findIndex((msg, i) => msg.role !== 'system' && !!msg.displayContent && i > firstMsgIdx) ?? ''
if (secondMsgIdx !== -1)
btnStringArr.push(truncate(allThreads[threadId].messages[secondMsgIdx].displayContent ?? ''))
const pastThread = allThreads[threadId];
let firstMsg = null;
// let secondMsg = null;
const numMessagesRemaining = allThreads[threadId].messages.filter((msg, i) => msg.role !== 'system' && !!msg.displayContent && i > secondMsgIdx).length
if (numMessagesRemaining > 0)
btnStringArr.push(numMessagesRemaining + '')
const firstMsgIdx = pastThread.messages.findIndex(
(msg) => msg.role !== 'system' && !!msg.displayContent
);
const btnString = btnStringArr.join(' / ')
if (firstMsgIdx !== -1) {
// firstMsg = truncate(pastThread.messages[firstMsgIdx].displayContent ?? '');
firstMsg = pastThread.messages[firstMsgIdx].displayContent ?? '';
} else {
firstMsg = '""';
}
// const secondMsgIdx = pastThread.messages.findIndex(
// (msg, i) => msg.role !== 'system' && !!msg.displayContent && i > firstMsgIdx
// );
// if (secondMsgIdx !== -1) {
// secondMsg = truncate(pastThread.messages[secondMsgIdx].displayContent ?? '');
// }
const numMessages = pastThread.messages.filter(
(msg) => msg.role !== 'system'
).length;
return (
<li key={pastThread.id}>
<button
type='button'
className={`
hover:bg-void-bg-1
${threadsState.currentThreadId === pastThread.id ? 'bg-void-bg-1' : ''}
rounded-sm px-2 py-1
w-full
text-left
flex items-center
`}
onClick={() => chatThreadsService.switchToThread(pastThread.id)}
onDoubleClick={() => sidebarStateService.setState({ isHistoryOpen: false })}
title={new Date(pastThread.createdAt).toLocaleString()}
>
<div className='truncate'>{`${firstMsg}`}</div>
<div>{`\u00A0(${numMessages})`}</div>
</button>
</li>
);
})
}
</ul>
return (
<button
key={pastThread.id}
className={`rounded-sm`}
onClick={() => threadsStateService.switchToThread(pastThread.id)}
title={new Date(pastThread.createdAt).toLocaleString()}
>
{btnString}
</button>
)
})}
</div>
</div>
@@ -1,7 +1,7 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
*-----------------------------------------------------------------------------------------*/
import { mountFnGenerator } from '../util/mountFnGenerator.js'
import { Sidebar } from './Sidebar.js'
@@ -1,7 +1,7 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
*-----------------------------------------------------------------------------------------*/
@tailwind base;
@tailwind components;
@@ -20,16 +20,6 @@
.inherit-bg-all-restyle > * {
background-color: inherit !important;
}
.bg-editor-style-override {
--vscode-sideBar-background: var(--vscode-editor-background);
}
/* html {
font-size: var(--vscode-font-size);
@@ -1,19 +0,0 @@
import { useCallback, useRef, useState } from 'react'
type ReturnType<T> = [
{ readonly current: T },
(t: T) => void
]
// use this if state might be too slow to catch
export const useRefState = <T,>(initVal: T): ReturnType<T> => {
const [_, _setState] = useState(false)
const ref = useRef<T>(initVal)
const setState = useCallback((newVal: T) => {
_setState(n => !n) // call rerender
ref.current = newVal
}, [])
return [ref, setState]
}
@@ -1,9 +1,9 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
*-----------------------------------------------------------------------------------------*/
import React, { forwardRef, MutableRefObject, useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';
import React, { useCallback, useEffect, useRef } from 'react';
import { IInputBoxStyles, InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js';
import { defaultCheckboxStyles, defaultInputBoxStyles, defaultSelectBoxStyles } from '../../../../../../../platform/theme/browser/defaultStyles.js';
import { SelectBox } from '../../../../../../../base/browser/ui/selectBox/selectBox.js';
@@ -12,10 +12,6 @@ import { Checkbox } from '../../../../../../../base/browser/ui/toggle/toggle.js'
import { CodeEditorWidget } from '../../../../../../../editor/browser/widget/codeEditor/codeEditorWidget.js'
import { useAccessor } from './services.js';
import { ITextModel } from '../../../../../../../editor/common/model.js';
import { asCssVariable } from '../../../../../../../platform/theme/common/colorUtils.js';
import { inputBackground, inputForeground } from '../../../../../../../platform/theme/common/colorRegistry.js';
import { useFloating, autoUpdate, offset, flip, shift, size, autoPlacement } from '@floating-ui/react';
// type guard
@@ -49,110 +45,8 @@ export const WidgetComponent = <CtorParams extends any[], Instance>({ ctor, prop
}
export type TextAreaFns = { setValue: (v: string) => void, enable: () => void, disable: () => void }
type InputBox2Props = {
initValue?: string | null;
placeholder: string;
multiline: boolean;
fnsRef?: { current: null | TextAreaFns };
className?: string;
onChangeText?: (value: string) => void;
onKeyDown?: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void;
onFocus?: (e: React.FocusEvent<HTMLTextAreaElement>) => void;
onBlur?: (e: React.FocusEvent<HTMLTextAreaElement>) => void;
onChangeHeight?: (newHeight: number) => void;
}
export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(function X({ initValue, placeholder, multiline, fnsRef, className, onKeyDown, onFocus, onBlur, onChangeText }, ref) {
// mirrors whatever is in ref
const textAreaRef = useRef<HTMLTextAreaElement | null>(null)
const [isEnabled, setEnabled] = useState(true)
const adjustHeight = useCallback(() => {
const r = textAreaRef.current
if (!r) return
r.style.height = 'auto' // set to auto to reset height, then set to new height
if (r.scrollHeight === 0) return requestAnimationFrame(adjustHeight)
const h = r.scrollHeight
const newHeight = Math.min(h + 1, 500) // plus one to avoid scrollbar appearing when it shouldn't
r.style.height = `${newHeight}px`
}, []);
const fns: TextAreaFns = useMemo(() => ({
setValue: (val) => {
const r = textAreaRef.current
if (!r) return
r.value = val
onChangeText?.(r.value)
adjustHeight()
},
enable: () => { setEnabled(true) },
disable: () => { setEnabled(false) },
}), [onChangeText, adjustHeight])
useEffect(() => {
if (initValue)
fns.setValue(initValue)
}, [initValue])
return (
<textarea
ref={useCallback((r: HTMLTextAreaElement | null) => {
if (fnsRef)
fnsRef.current = fns
textAreaRef.current = r
if (typeof ref === 'function') ref(r)
else if (ref) ref.current = r
adjustHeight()
}, [fnsRef, fns, setEnabled, adjustHeight, ref])}
onFocus={onFocus}
onBlur={onBlur}
disabled={!isEnabled}
className={`w-full resize-none max-h-[500px] overflow-y-auto text-void-fg-1 placeholder:text-void-fg-3 ${className}`}
style={{
// defaultInputBoxStyles
background: asCssVariable(inputBackground),
color: asCssVariable(inputForeground)
// inputBorder: asCssVariable(inputBorder),
}}
onChange={useCallback(() => {
const r = textAreaRef.current
if (!r) return
onChangeText?.(r.value)
adjustHeight()
}, [onChangeText, adjustHeight])}
onKeyDown={useCallback((e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter') {
// Shift + Enter when multiline = newline
const shouldAddNewline = e.shiftKey && multiline
if (!shouldAddNewline) e.preventDefault(); // prevent newline from being created
}
onKeyDown?.(e)
}, [onKeyDown, multiline])}
rows={1}
placeholder={placeholder}
/>
)
})
export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, placeholder, multiline }: {
export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, placeholder, multiline, styles }: {
onChangeText: (value: string) => void;
styles?: Partial<IInputBoxStyles>,
onCreateInstance?: (instance: InputBox) => void | IDisposable[];
@@ -166,10 +60,6 @@ export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, plac
const contextViewProvider = accessor.get('IContextViewService')
return <WidgetComponent
ctor={InputBox}
className='
bg-void-bg-1
@@[&_::placeholder]:!void-text-void-fg-3
'
propsFn={useCallback((container) => [
container,
contextViewProvider,
@@ -179,6 +69,7 @@ export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, plac
inputForeground: "var(--vscode-foreground)",
// inputBackground: 'transparent',
// inputBorder: 'none',
...styles,
},
placeholder,
tooltip: '',
@@ -302,220 +193,11 @@ export const VoidCheckBox = ({ label, value, onClick, className }: { label: stri
}
export const VoidCustomDropdownBox = <T extends any>({
options,
selectedOption,
onChangeOption,
getOptionDropdownName,
getOptionDisplayName,
getOptionsEqual,
className,
arrowTouchesText = true,
matchInputWidth = false,
gap = 0,
}: {
options: T[];
selectedOption: T | undefined;
onChangeOption: (newValue: T) => void;
getOptionDropdownName: (option: T) => string;
getOptionDisplayName: (option: T) => string;
getOptionsEqual: (a: T, b: T) => boolean;
className?: string;
arrowTouchesText?: boolean;
matchInputWidth?: boolean;
gap?: number;
}) => {
const [isOpen, setIsOpen] = useState(false);
const measureRef = useRef<HTMLDivElement>(null);
// Replace manual positioning with floating-ui
const {
x,
y,
strategy,
refs,
middlewareData,
update
} = useFloating({
open: isOpen,
onOpenChange: setIsOpen,
placement: 'bottom-start',
middleware: [
offset(gap),
flip({
boundary: document.body,
padding: 8
}),
shift({
boundary: document.body,
padding: 8,
}),
size({
apply({ availableHeight, elements, rects }) {
const maxHeight = Math.min(availableHeight)
Object.assign(elements.floating.style, {
maxHeight: `${maxHeight}px`,
overflowY: 'auto',
// Ensure the width isn't constrained by the parent
width: `${Math.max(
rects.reference.width,
measureRef.current?.offsetWidth ?? 0
)}px`
});
},
padding: 8,
// Use viewport as boundary instead of any parent element
boundary: document.body,
}),
],
whileElementsMounted: autoUpdate,
strategy: 'fixed',
});
// if the selected option is null, set the selection to the 0th option
useEffect(() => {
if (options.length === 0) return
if (selectedOption) return
onChangeOption(options[0])
}, [selectedOption, onChangeOption, options])
// Handle clicks outside
useEffect(() => {
if (!isOpen) return;
const handleClickOutside = (event: MouseEvent) => {
const target = event.target as Node;
const floating = refs.floating.current;
const reference = refs.reference.current;
// Check if reference is an HTML element before using contains
const isReferenceHTMLElement = reference && 'contains' in reference;
if (
floating &&
(!isReferenceHTMLElement || !reference.contains(target)) &&
!floating.contains(target)
) {
setIsOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, [isOpen, refs.floating, refs.reference]);
if (!selectedOption)
return null
return (
<div className={`inline-block relative ${className}`}>
{/* Hidden measurement div */}
<div
ref={measureRef}
className="opacity-0 pointer-events-none absolute -left-[999999px] -top-[999999px] flex flex-col"
aria-hidden="true"
>
{options.map((option) => (
<div key={getOptionDropdownName(option)} className="flex items-center whitespace-nowrap">
<div className="w-4" />
<span className="px-2">{getOptionDropdownName(option)}</span>
</div>
))}
</div>
{/* Select Button */}
<button
type='button'
ref={refs.setReference}
className="flex items-center h-4 bg-transparent whitespace-nowrap hover:brightness-90 w-full"
onClick={() => setIsOpen(!isOpen)}
>
<span className={`max-w-[120px] truncate ${arrowTouchesText ? 'mr-1' : ''}`}>
{getOptionDisplayName(selectedOption)}
</span>
<svg
className={`size-3 flex-shrink-0 ${arrowTouchesText ? '' : 'ml-auto'}`}
viewBox="0 0 12 12"
fill="none"
>
<path
d="M2.5 4.5L6 8L9.5 4.5"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</button>
{/* Dropdown Menu */}
{isOpen && (
<div
ref={refs.setFloating}
className="z-10 bg-void-bg-1 border-void-border-1 border overflow-hidden rounded shadow-lg"
style={{
position: strategy,
top: y ?? 0,
left: x ?? 0,
width: matchInputWidth
? (refs.reference.current instanceof HTMLElement ? refs.reference.current.offsetWidth : 0)
: Math.max(
(refs.reference.current instanceof HTMLElement ? refs.reference.current.offsetWidth : 0),
(measureRef.current instanceof HTMLElement ? measureRef.current.offsetWidth : 0)
),
}}
>
{options.map((option) => {
const thisOptionIsSelected = getOptionsEqual(option, selectedOption);
const optionName = getOptionDropdownName(option);
return (
<div
key={optionName}
className={`flex items-center px-2 py-1 cursor-pointer whitespace-nowrap
transition-all duration-100
bg-void-bg-1
${thisOptionIsSelected ? 'bg-void-bg-2' : 'hover:bg-void-bg-2'}
`}
onClick={() => {
onChangeOption(option);
setIsOpen(false);
}}
>
<div className="w-4 flex justify-center flex-shrink-0">
{thisOptionIsSelected && (
<svg className="size-3" viewBox="0 0 12 12" fill="none">
<path
d="M10 3L4.5 8.5L2 6"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)}
</div>
<span>{optionName}</span>
</div>
);
})}
</div>
)}
</div>
);
};
export const _VoidSelectBox = <T,>({ onChangeSelection, onCreateInstance, selectBoxRef, options, className }: {
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 }[];
className?: string;
}) => {
const accessor = useAccessor()
const contextViewProvider = accessor.get('IContextViewService')
@@ -523,13 +205,7 @@ export const _VoidSelectBox = <T,>({ onChangeSelection, onCreateInstance, select
let containerRef = useRef<HTMLDivElement | null>(null);
return <WidgetComponent
className={`
@@select-child-restyle
@@[&_select]:!void-text-void-fg-3
@@[&_select]:!void-text-xs
!text-void-fg-3
${className ?? ''}
`}
className='@@select-child-restyle'
ctor={SelectBox}
propsFn={useCallback((container) => {
containerRef.current = container
@@ -538,15 +214,14 @@ export const _VoidSelectBox = <T,>({ onChangeSelection, onCreateInstance, select
options.map(opt => ({ text: opt.text })),
defaultIndex,
contextViewProvider,
defaultSelectBoxStyles,
defaultSelectBoxStyles
] as const;
}, [containerRef, options])}
}, [containerRef, options, contextViewProvider])}
dispose={useCallback((instance: SelectBox) => {
instance.dispose();
containerRef.current?.childNodes.forEach(child => {
for (let child of containerRef.current?.childNodes ?? [])
containerRef.current?.removeChild(child)
})
}, [containerRef])}
onCreateInstance={useCallback((instance: SelectBox) => {
@@ -611,48 +286,24 @@ const normalizeIndentation = (code: string): string => {
}
export const VoidCodeEditor = ({ initValue, language }: { initValue: string, language: string | undefined }) => {
const modelOfEditorId: { [id: string]: ITextModel | undefined } = {}
export type VoidCodeEditorProps = { initValue: string, language?: string, maxHeight?: number, showScrollbars?: boolean }
export const VoidCodeEditor = ({ initValue, language, maxHeight, showScrollbars }: VoidCodeEditorProps) => {
initValue = normalizeIndentation(initValue)
// default settings
const MAX_HEIGHT = maxHeight ?? Infinity;
const SHOW_SCROLLBARS = showScrollbars ?? false;
const MAX_HEIGHT = Infinity;
const divRef = useRef<HTMLDivElement | null>(null)
const accessor = useAccessor()
const instantiationService = accessor.get('IInstantiationService')
// const languageDetectionService = accessor.get('ILanguageDetectionService')
const modelService = accessor.get('IModelService')
const languageDetectionService = accessor.get('ILanguageDetectionService')
initValue = normalizeIndentation(initValue)
const id = useId()
// these are used to pass to the model creation of modelRef
const initValueRef = useRef(initValue)
const languageRef = useRef(language)
const modelRef = useRef<ITextModel | null>(null)
// if we change the initial value, don't re-render the whole thing, just set it here. same for language
useEffect(() => {
initValueRef.current = initValue
modelRef.current?.setValue(initValue)
}, [initValue])
useEffect(() => {
languageRef.current = language
if (language) modelRef.current?.setLanguage(language)
}, [language])
return <div ref={divRef} className='relative z-0 px-2 py-1 bg-void-bg-3'>
return <div ref={divRef}>
<WidgetComponent
className='@@bg-editor-style-override' // text-sm
ctor={useCallback((container) => {
return instantiationService.createInstance(
className='relative z-0 text-sm bg-vscode-editor-bg'
ctor={useCallback((container) =>
instantiationService.createInstance(
CodeEditorWidget,
container,
{
@@ -661,19 +312,10 @@ export const VoidCodeEditor = ({ initValue, language, maxHeight, showScrollbars
scrollbar: {
alwaysConsumeMouseWheel: false,
...SHOW_SCROLLBARS ? {
vertical: 'auto',
verticalScrollbarSize: 8,
horizontal: 'auto',
horizontalScrollbarSize: 8,
} : {
vertical: 'hidden',
verticalScrollbarSize: 0,
horizontal: 'auto',
horizontalScrollbarSize: 8,
ignoreHorizontalScrollbarInContentHeight: true,
},
vertical: 'hidden',
horizontal: 'hidden',
verticalScrollbarSize: 0,
horizontalScrollbarSize: 0,
},
scrollBeyondLastLine: false,
@@ -688,8 +330,6 @@ export const VoidCodeEditor = ({ initValue, language, maxHeight, showScrollbars
// maxColumn: 0,
},
hover: { enabled: false },
selectionHighlight: false, // highlights whole words
renderLineHighlight: 'none',
@@ -707,25 +347,27 @@ export const VoidCodeEditor = ({ initValue, language, maxHeight, showScrollbars
{
isSimpleWidget: true,
})
}, [instantiationService])}
, [instantiationService])
}
onCreateInstance={useCallback((editor: CodeEditorWidget) => {
const model = modelOfEditorId[id] ?? modelService.createModel(
initValueRef.current, {
languageId: languageRef.current ? languageRef.current : 'typescript',
onDidChange: (e) => { return { dispose: () => { } } } // no idea why they'd require this
})
modelRef.current = model
const model = modelService.createModel(
initValue,
language ? {
languageId: language,
onDidChange: () => ({
dispose: () => { }
})
} : null
);
editor.setModel(model);
const container = editor.getDomNode()
const parentNode = container?.parentElement
const resize = () => {
const height = editor.getScrollHeight() + 1
if (parentNode) {
// const height = Math.min(, MAX_HEIGHT);
const height = Math.min(editor.getScrollHeight() + 1, MAX_HEIGHT);
parentNode.style.height = `${height}px`;
parentNode.style.maxHeight = `${MAX_HEIGHT}px`;
editor.layout();
}
}
@@ -733,12 +375,12 @@ export const VoidCodeEditor = ({ initValue, language, maxHeight, showScrollbars
resize()
const disposable = editor.onDidContentSizeChange(() => { resize() });
return [disposable, model]
}, [modelService])}
return [disposable]
}, [modelService, initValue, language])}
dispose={useCallback((editor: CodeEditorWidget) => {
editor.dispose();
}, [modelService])}
}, [modelService, languageDetectionService])}
propsFn={useCallback(() => { return [] }, [])}
/>
@@ -747,13 +389,6 @@ export const VoidCodeEditor = ({ initValue, language, maxHeight, showScrollbars
}
export const VoidButton = ({ children, disabled, onClick }: { children: React.ReactNode; disabled?: boolean; onClick: () => void }) => {
return <button disabled={disabled}
className='px-3 py-1 bg-black/10 dark:bg-gray-200/10 rounded-sm overflow-hidden'
onClick={onClick}
>{children}</button>
}
// export const VoidScrollableElt = ({ options, children }: { options: ScrollableElementCreationOptions, children: React.ReactNode }) => {
// const instanceRef = useRef<DomScrollableElement | null>(null);
// const [childrenPortal, setChildrenPortal] = useState<React.ReactNode | null>(null)
@@ -1,7 +1,7 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
*-----------------------------------------------------------------------------------------*/
import React, { useEffect, useState } from 'react';
import * as ReactDOM from 'react-dom/client'
@@ -1,18 +1,17 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------
* Copyright (c) 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the MIT License. See LICENSE.txt in the project root for more information.
*-----------------------------------------------------------------------------------------*/
import React, { useState, useEffect } from 'react'
import { ThreadStreamState, ThreadsState } from '../../../chatThreadService.js'
import { RefreshableProviderName, SettingsOfProvider } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js'
import { ThreadsState } from '../../../threadHistoryService.js'
import { RefreshableProviderName, SettingsOfProvider } from '../../../../../../../platform/void/common/voidSettingsTypes.js'
import { IDisposable } from '../../../../../../../base/common/lifecycle.js'
import { VoidSidebarState } from '../../../sidebarStateService.js'
import { VoidSettingsState } from '../../../../../../../workbench/contrib/void/common/voidSettingsService.js'
import { VoidSettingsState } from '../../../../../../../platform/void/common/voidSettingsService.js'
import { ColorScheme } from '../../../../../../../platform/theme/common/theme.js'
import { VoidUriState } from '../../../voidUriStateService.js';
import { VoidQuickEditState } from '../../../quickEditStateService.js'
import { RefreshModelStateOfProvider } from '../../../../../../../workbench/contrib/void/common/refreshModelService.js'
import { RefreshModelStateOfProvider } from '../../../../../../../platform/void/common/refreshModelService.js'
@@ -25,14 +24,13 @@ import { IContextViewService, IContextMenuService } from '../../../../../../../p
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 '../../../../../../../workbench/contrib/void/common/llmMessageService.js';
import { IRefreshModelService } from '../../../../../../../workbench/contrib/void/common/refreshModelService.js';
import { IVoidSettingsService } from '../../../../../../../workbench/contrib/void/common/voidSettingsService.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 { IVoidUriStateService } from '../../../voidUriStateService.js';
import { IQuickEditStateService } from '../../../quickEditStateService.js';
import { ISidebarStateService } from '../../../sidebarStateService.js';
import { IChatThreadService } from '../../../chatThreadService.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'
@@ -42,11 +40,6 @@ import { IAccessibilityService } from '../../../../../../../platform/accessibili
import { ILanguageConfigurationService } from '../../../../../../../editor/common/languages/languageConfigurationRegistry.js'
import { ILanguageFeaturesService } from '../../../../../../../editor/common/services/languageFeatures.js'
import { ILanguageDetectionService } from '../../../../../../services/languageDetection/common/languageDetectionWorkerService.js'
import { IKeybindingService } from '../../../../../../../platform/keybinding/common/keybinding.js'
import { IEnvironmentService } from '../../../../../../../platform/environment/common/environment.js'
import { IConfigurationService } from '../../../../../../../platform/configuration/common/configuration.js'
import { IPathService } from '../../../../../../../workbench/services/path/common/pathService.js'
import { IMetricsService } from '../../../../../../../workbench/contrib/void/common/metricsService.js'
@@ -54,20 +47,14 @@ import { IMetricsService } from '../../../../../../../workbench/contrib/void/com
// even if React hasn't mounted yet, the variables are always updated to the latest state.
// React listens by adding a setState function to these listeners.
let uriState: VoidUriState
const uriStateListeners: Set<(s: VoidUriState) => void> = new Set()
let quickEditState: VoidQuickEditState
const quickEditStateListeners: Set<(s: VoidQuickEditState) => void> = new Set()
let sidebarState: VoidSidebarState
const sidebarStateListeners: Set<(s: VoidSidebarState) => void> = new Set()
let chatThreadsState: ThreadsState
const chatThreadsStateListeners: Set<(s: ThreadsState) => void> = new Set()
let chatThreadsStreamState: ThreadStreamState
const chatThreadsStreamStateListeners: Set<(threadId: string) => void> = new Set()
let threadsState: ThreadsState
const threadsStateListeners: Set<(s: ThreadsState) => void> = new Set()
let settingsState: VoidSettingsState
const settingsStateListeners: Set<(s: VoidSettingsState) => void> = new Set()
@@ -96,25 +83,15 @@ export const _registerServices = (accessor: ServicesAccessor) => {
_registerAccessor(accessor)
const stateServices = {
uriStateService: accessor.get(IVoidUriStateService),
quickEditStateService: accessor.get(IQuickEditStateService),
sidebarStateService: accessor.get(ISidebarStateService),
chatThreadsStateService: accessor.get(IChatThreadService),
threadsStateService: accessor.get(IThreadHistoryService),
settingsStateService: accessor.get(IVoidSettingsService),
refreshModelService: accessor.get(IRefreshModelService),
themeService: accessor.get(IThemeService),
inlineDiffsService: accessor.get(IInlineDiffsService),
}
const { uriStateService, sidebarStateService, quickEditStateService, settingsStateService, chatThreadsStateService, refreshModelService, themeService, inlineDiffsService } = stateServices
uriState = uriStateService.state
disposables.push(
uriStateService.onDidChangeState(() => {
uriState = uriStateService.state
uriStateListeners.forEach(l => l(uriState))
})
)
const { sidebarStateService, quickEditStateService, settingsStateService, threadsStateService, refreshModelService, themeService, } = stateServices
quickEditState = quickEditStateService.state
disposables.push(
@@ -132,20 +109,11 @@ export const _registerServices = (accessor: ServicesAccessor) => {
})
)
chatThreadsState = chatThreadsStateService.state
threadsState = threadsStateService.state
disposables.push(
chatThreadsStateService.onDidChangeCurrentThread(() => {
chatThreadsState = chatThreadsStateService.state
chatThreadsStateListeners.forEach(l => l(chatThreadsState))
})
)
// same service, different state
chatThreadsStreamState = chatThreadsStateService.streamState
disposables.push(
chatThreadsStateService.onDidChangeStreamState(({ threadId }) => {
chatThreadsStreamState = chatThreadsStateService.streamState
chatThreadsStreamStateListeners.forEach(l => l(threadId))
threadsStateService.onDidChangeCurrentThread(() => {
threadsState = threadsStateService.state
threadsStateListeners.forEach(l => l(threadsState))
})
)
@@ -174,7 +142,6 @@ export const _registerServices = (accessor: ServicesAccessor) => {
})
)
return disposables
}
@@ -193,10 +160,9 @@ const getReactAccessor = (accessor: ServicesAccessor) => {
IRefreshModelService: accessor.get(IRefreshModelService),
IVoidSettingsService: accessor.get(IVoidSettingsService),
IInlineDiffsService: accessor.get(IInlineDiffsService),
IVoidUriStateService: accessor.get(IVoidUriStateService),
IQuickEditStateService: accessor.get(IQuickEditStateService),
ISidebarStateService: accessor.get(ISidebarStateService),
IChatThreadService: accessor.get(IChatThreadService),
IThreadHistoryService: accessor.get(IThreadHistoryService),
IInstantiationService: accessor.get(IInstantiationService),
ICodeEditorService: accessor.get(ICodeEditorService),
@@ -207,12 +173,6 @@ const getReactAccessor = (accessor: ServicesAccessor) => {
ILanguageConfigurationService: accessor.get(ILanguageConfigurationService),
ILanguageDetectionService: accessor.get(ILanguageDetectionService),
ILanguageFeaturesService: accessor.get(ILanguageFeaturesService),
IKeybindingService: accessor.get(IKeybindingService),
IEnvironmentService: accessor.get(IEnvironmentService),
IConfigurationService: accessor.get(IConfigurationService),
IPathService: accessor.get(IPathService),
IMetricsService: accessor.get(IMetricsService),
} as const
return reactAccessor
@@ -240,16 +200,6 @@ export const useAccessor = () => {
// -- state of services --
export const useUriState = () => {
const [s, ss] = useState(uriState)
useEffect(() => {
ss(uriState)
uriStateListeners.add(ss)
return () => { uriStateListeners.delete(ss) }
}, [ss])
return s
}
export const useQuickEditState = () => {
const [s, ss] = useState(quickEditState)
useEffect(() => {
@@ -280,47 +230,17 @@ export const useSettingsState = () => {
return s
}
export const useChatThreadsState = () => {
const [s, ss] = useState(chatThreadsState)
export const useThreadsState = () => {
const [s, ss] = useState(threadsState)
useEffect(() => {
ss(chatThreadsState)
chatThreadsStateListeners.add(ss)
return () => { chatThreadsStateListeners.delete(ss) }
ss(threadsState)
threadsStateListeners.add(ss)
return () => { threadsStateListeners.delete(ss) }
}, [ss])
return s
// allow user to set state natively in react
// const ss: React.Dispatch<React.SetStateAction<ThreadsState>> = (action)=>{
// _ss(action)
// if (typeof action === 'function') {
// const newState = action(chatThreadsState)
// chatThreadsState = newState
// } else {
// chatThreadsState = action
// }
// }
// return [s, ss] as const
}
export const useChatThreadsStreamState = (threadId: string) => {
const [s, ss] = useState<ThreadStreamState[string] | undefined>(chatThreadsStreamState[threadId])
useEffect(() => {
ss(chatThreadsStreamState[threadId])
const listener = (threadId_: string) => {
if (threadId_ !== threadId) return
ss(chatThreadsStreamState[threadId])
}
chatThreadsStreamStateListeners.add(listener)
return () => { chatThreadsStreamStateListeners.delete(listener) }
}, [ss, threadId])
return s
}
export const useRefreshModelState = () => {
const [s, ss] = useState(refreshModelState)
useEffect(() => {

Some files were not shown because too many files have changed in this diff Show More