Compare commits
185 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 41fe5c50e2 | |||
| 36b1b56690 | |||
| 249eee341d | |||
| 198a948f6c | |||
| 7dcb08b09a | |||
| efeb85b48b | |||
| d698e6f3c1 | |||
| 2161d740df | |||
| 96b5b72d30 | |||
| e2e8279fce | |||
| 5ed1f465a2 | |||
| 995357710e | |||
| a98106072c | |||
| c1be466467 | |||
| 9cbe83b57c | |||
| e46b295449 | |||
| 3fd3634257 | |||
| 231990088a | |||
| 20da82762e | |||
| 0d9be68aff | |||
| 165b5a4fd2 | |||
| 812014d6d3 | |||
| d3e73b92e0 | |||
| 00da05f24f | |||
| e4694256eb | |||
| 18b278577d | |||
| a252bb7a30 | |||
| c240360710 | |||
| 893308c38e | |||
| 48defe849d | |||
| 3c03f58488 | |||
| 2470e0de7f | |||
| 387fd64db0 | |||
| 4ffd269db5 | |||
| 5ce25e20c3 | |||
| 63efa219f5 | |||
| 826d148305 | |||
| 323d80ef55 | |||
| 902588638d | |||
| a1ab094652 | |||
| 902b460b9c | |||
| 073205ace3 | |||
| 98c1d81875 | |||
| 2f49650e15 | |||
| 1be9300dc4 | |||
| d9b77b67b2 | |||
| 2f674f5323 | |||
| c8fa39e552 | |||
| 3b73e3ab26 | |||
| c606ac4361 | |||
| da4c9c8c40 | |||
| 31ed11393f | |||
| 1c5157d97e | |||
| cf241867d8 | |||
| 5a9f9f70d9 | |||
| 19ff19fb80 | |||
| 87076ed586 | |||
| 473250d8bc | |||
| cd983f0dd9 | |||
| 5c4753555e | |||
| 8f79222b8b | |||
| 145fd9614b | |||
| 41057c20fc | |||
| 528b8a6b9b | |||
| e3941d5565 | |||
| 4d11a4fd29 | |||
| 3baaf8d6c4 | |||
| 0ec0c324b1 | |||
| aa580351d9 | |||
| 570e13127d | |||
| fb7b50b36b | |||
| 9cd8295dab | |||
| 9ec13d2ad8 | |||
| d9cf21448c | |||
| 27d009314b | |||
| 5fdd21f12b | |||
| dc5c7e9a46 | |||
| d1c5cf518e | |||
| c1123f2cfd | |||
| fb0bb0ca31 | |||
| 4863802713 | |||
| 5589b2e302 | |||
| 327dbb937b | |||
| a3e62ee6f4 | |||
| d7a6611d89 | |||
| e4bb15ef64 | |||
| a98c77572e | |||
| e4d747d0a6 | |||
| 01b1358a01 | |||
| c7a6bddfbe | |||
| 55ea8e61c6 | |||
| 4f7fd66c2f | |||
| 4547363dde | |||
| a1dfeb0f00 | |||
| b036a8c59c | |||
| 72cd23f0ac | |||
| 49c2bcb848 | |||
| 97ea0f77f9 | |||
| 0cd6e0fb67 | |||
| 7c227e237f | |||
| 3089bb31b5 | |||
| 018fd016c8 | |||
| f69f90b4c8 | |||
| 000747eb88 | |||
| 74022e0306 | |||
| 21708fd7e0 | |||
| 5593ff5d79 | |||
| 197067af5a | |||
| eec4f6c95e | |||
| 3e045117d6 | |||
| 67c557b7b9 | |||
| 5ffd569fd8 | |||
| ff83bb5edc | |||
| a3f145328b | |||
| 02894c12de | |||
| 52b6c81507 | |||
| 5df5f37a6a | |||
| f247b76a6e | |||
| d1c659be9d | |||
| a6b4955f8d | |||
| 03b0b98eed | |||
| 7f7d30e40b | |||
| f68bc52023 | |||
| 520c971e3e | |||
| 657e752987 | |||
| 458a5160b0 | |||
| a90bbffeb9 | |||
| 99ff885ce2 | |||
| a97047ed43 | |||
| 00f10c283e | |||
| 57628ec244 | |||
| c54dcd0b0c | |||
| e957932f39 | |||
| fea9d380cc | |||
| f2318cc8a2 | |||
| 8264cccd04 | |||
| a1ca0f9291 | |||
| 58f06d3ebb | |||
| e06eda1cc4 | |||
| f4d72e3ee8 | |||
| b3837a3099 | |||
| c6ae6d5152 | |||
| 2d43dbded1 | |||
| a34d652a1d | |||
| a0569b1ead | |||
| b3ebe85c04 | |||
| 0919775e77 | |||
| da6636a948 | |||
| 6327c584cc | |||
| f7cf44136b | |||
| c8635a1bdb | |||
| d7501112f5 | |||
| d7ca99e23d | |||
| 387be4db76 | |||
| 2aec37fc57 | |||
| 7e5cc27349 | |||
| 151cdb13e6 | |||
| d54ccff4a2 | |||
| c2f358bd5e | |||
| 198f04d87a | |||
| 326e75eb45 | |||
| 7891a908ff | |||
| 6970ec9fd2 | |||
| fd4f3c3ab6 | |||
| aa2394e207 | |||
| 24e276ba47 | |||
| 4e0dd8f8db | |||
| c239682ad8 | |||
| cd9bb6a07d | |||
| fd989b2c13 | |||
| 3bfb50e6ee | |||
| 4b8afc1771 | |||
| d067966b58 | |||
| 278c52bd35 | |||
| 2701226ef3 | |||
| 9abc3ad3dc | |||
| af85949561 | |||
| 4e392bee8e | |||
| 5453acdb19 | |||
| 7047459e57 | |||
| 4065c5b504 | |||
| 4729e4dac3 | |||
| 1e975ecc3b | |||
| 8476a99903 | |||
| 85314f8d47 |
@@ -22,3 +22,5 @@ product.overrides.json
|
||||
*.snap.actual
|
||||
.vscode-test
|
||||
.tmp/
|
||||
.tmp2/
|
||||
.tool-versions
|
||||
|
||||
Vendored
+30
-1
@@ -29,6 +29,34 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"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",
|
||||
@@ -61,7 +89,8 @@
|
||||
"label": "VS Code - Build",
|
||||
"dependsOn": [
|
||||
"Core - Build",
|
||||
"Ext - Build"
|
||||
"Ext - Build",
|
||||
"React - Build"
|
||||
],
|
||||
"group": {
|
||||
"kind": "build",
|
||||
|
||||
+23
-14
@@ -1,16 +1,26 @@
|
||||
# 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:
|
||||
|
||||
- 👨💻 Build new features - see [Issues](https://github.com/voideditor/void/issues).
|
||||
- 💫 Complete items on the [Roadmap](https://github.com/orgs/voideditor/projects/2).
|
||||
- 💡 Make suggestions in our [Discord](https://discord.gg/RSNjgaugJs).
|
||||
- ⭐️ If you want to build your AI tool into Void, feel free to get in touch! It's very easy to extend Void, and the UX you create will be much more natural than a VSCode Extension.
|
||||
- 🪴 Start new Issues - see [Issues](https://github.com/voideditor/void/issues).
|
||||
|
||||
|
||||
|
||||
### 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/`.
|
||||
|
||||
|
||||
Void's code mostly lives in `src/vs/workbench/contrib/void/` and `src/vs/platform/void/`.
|
||||
|
||||
|
||||
## Building Void
|
||||
@@ -75,7 +85,7 @@ Alternatively, if you want to build Void from the terminal, instead of pressing
|
||||
#### Common Fixes
|
||||
|
||||
- Make sure you followed the prerequisite steps.
|
||||
- Make sure you have the same NodeJS version as `.nvmrc`.
|
||||
- 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.
|
||||
@@ -111,16 +121,19 @@ workspace/
|
||||
```
|
||||
|
||||
### Distributing
|
||||
We have a few scripts to convert `Mac .app -> .dmg`, `Windows folder -> .exe`, and `Linux folder -> appimage` for distribution. Let us know if you'd like access to them.
|
||||
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.
|
||||
- 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 🙂
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<!--
|
||||
# Relevant files
|
||||
|
||||
@@ -149,7 +162,3 @@ Edit: far too many changes to track... this is old
|
||||
- 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).
|
||||
|
||||
@@ -9,31 +9,31 @@
|
||||
/>
|
||||
</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/) for downloading the official release. If you're new, welcome!
|
||||
Void is the open-source Cursor alternative.
|
||||
|
||||
This repo contains the full sourcecode for Void. We are currently in [open beta](https://voideditor.com/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)
|
||||
|
||||
- 🔨 [Contribute](https://github.com/voideditor/void/blob/main/CONTRIBUTING.md)
|
||||
|
||||
|
||||
<!-- ❤️ Setup: -->
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Feel free to attend a weekly meeting in our Discord channel if you'd like to contribute!
|
||||
|
||||
1. To contribute to the Void repository, follow the steps in [`CONTRIBUTING.md`](https://github.com/voideditor/void/blob/main/CONTRIBUTING.md).
|
||||
|
||||
2. To suggest a new feature, please create a new [GitHub Issue](https://github.com/voideditor/void/issues). Suggestions will be added to our Roadmap within one day.
|
||||
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.
|
||||
|
||||
3. We're open to collaborations of all types. Feel free to contact us via email or discord.
|
||||
|
||||
## Reference
|
||||
|
||||
Void is a fork of the [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.
|
||||
Feel free to reach out in our Discord or contact us via email: hello@voideditor.com.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
#!/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!"
|
||||
Generated
+119
-40
@@ -11,9 +11,11 @@
|
||||
"license": "MIT",
|
||||
"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",
|
||||
@@ -65,7 +67,8 @@
|
||||
"vscode-regexpp": "^3.1.0",
|
||||
"vscode-textmate": "9.1.0",
|
||||
"yauzl": "^3.0.0",
|
||||
"yazl": "^2.4.3"
|
||||
"yazl": "^2.4.3",
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.46.1",
|
||||
@@ -1549,6 +1552,65 @@
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/core": {
|
||||
"version": "1.6.9",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz",
|
||||
"integrity": "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/utils": "^0.2.9"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/dom": {
|
||||
"version": "1.6.13",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz",
|
||||
"integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/core": "^1.6.0",
|
||||
"@floating-ui/utils": "^0.2.9"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/react": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.3.tgz",
|
||||
"integrity": "sha512-CLHnes3ixIFFKVQDdICjel8muhFLOBdQH7fgtHNPY8UbCNqbeKZ262G7K66lGQOUQWWnYocf7ZbUsLJgGfsLHg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/react-dom": "^2.1.2",
|
||||
"@floating-ui/utils": "^0.2.9",
|
||||
"tabbable": "^6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=17.0.0",
|
||||
"react-dom": ">=17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/react-dom": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz",
|
||||
"integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "^1.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/react/node_modules/tabbable": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz",
|
||||
"integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@floating-ui/utils": {
|
||||
"version": "0.2.9",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz",
|
||||
"integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@google/generative-ai": {
|
||||
"version": "0.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.21.0.tgz",
|
||||
@@ -2366,17 +2428,25 @@
|
||||
"exenv-es6": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@mistralai/mistralai": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-1.4.0.tgz",
|
||||
"integrity": "sha512-xA3DAtIDh4Qgr1EoSuiGVE+2ABNrxpcTeC0kSXYbkDNUGdthalLAH7DgbG0fkKZ7TN8xdWXQq2WiIghp/O96Eg==",
|
||||
"peerDependencies": {
|
||||
"zod": ">= 3"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/env": {
|
||||
"version": "15.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.1.4.tgz",
|
||||
"integrity": "sha512-2fZ5YZjedi5AGaeoaC0B20zGntEHRhi2SdWcu61i48BllODcAmmtj8n7YarSPt4DaTsJaBFdxQAVEVzgmx2Zpw==",
|
||||
"version": "15.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.1.6.tgz",
|
||||
"integrity": "sha512-d9AFQVPEYNr+aqokIiPLNK/MTyt3DWa/dpKveiAaVccUadFbhFEvY6FXYX2LJO2Hv7PHnLBu2oWwB4uBuHjr/w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@next/swc-darwin-arm64": {
|
||||
"version": "15.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.1.4.tgz",
|
||||
"integrity": "sha512-wBEMBs+np+R5ozN1F8Y8d/Dycns2COhRnkxRc+rvnbXke5uZBHkUGFgWxfTXn5rx7OLijuUhyfB+gC/ap58dDw==",
|
||||
"version": "15.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.1.6.tgz",
|
||||
"integrity": "sha512-u7lg4Mpl9qWpKgy6NzEkz/w0/keEHtOybmIl0ykgItBxEM5mYotS5PmqTpo+Rhg8FiOiWgwr8USxmKQkqLBCrw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2391,9 +2461,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-darwin-x64": {
|
||||
"version": "15.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.1.4.tgz",
|
||||
"integrity": "sha512-7sgf5rM7Z81V9w48F02Zz6DgEJulavC0jadab4ZsJ+K2sxMNK0/BtF8J8J3CxnsJN3DGcIdC260wEKssKTukUw==",
|
||||
"version": "15.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.1.6.tgz",
|
||||
"integrity": "sha512-x1jGpbHbZoZ69nRuogGL2MYPLqohlhnT9OCU6E6QFewwup+z+M6r8oU47BTeJcWsF2sdBahp5cKiAcDbwwK/lg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2408,9 +2478,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-gnu": {
|
||||
"version": "15.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.1.4.tgz",
|
||||
"integrity": "sha512-JaZlIMNaJenfd55kjaLWMfok+vWBlcRxqnRoZrhFQrhM1uAehP3R0+Aoe+bZOogqlZvAz53nY/k3ZyuKDtT2zQ==",
|
||||
"version": "15.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.1.6.tgz",
|
||||
"integrity": "sha512-jar9sFw0XewXsBzPf9runGzoivajeWJUc/JkfbLTC4it9EhU8v7tCRLH7l5Y1ReTMN6zKJO0kKAGqDk8YSO2bg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2425,9 +2495,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-musl": {
|
||||
"version": "15.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.1.4.tgz",
|
||||
"integrity": "sha512-7EBBjNoyTO2ipMDgCiORpwwOf5tIueFntKjcN3NK+GAQD7OzFJe84p7a2eQUeWdpzZvhVXuAtIen8QcH71ZCOQ==",
|
||||
"version": "15.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.1.6.tgz",
|
||||
"integrity": "sha512-+n3u//bfsrIaZch4cgOJ3tXCTbSxz0s6brJtU3SzLOvkJlPQMJ+eHVRi6qM2kKKKLuMY+tcau8XD9CJ1OjeSQQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2442,9 +2512,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-gnu": {
|
||||
"version": "15.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.1.4.tgz",
|
||||
"integrity": "sha512-9TGEgOycqZFuADyFqwmK/9g6S0FYZ3tphR4ebcmCwhL8Y12FW8pIBKJvSwV+UBjMkokstGNH+9F8F031JZKpHw==",
|
||||
"version": "15.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.1.6.tgz",
|
||||
"integrity": "sha512-SpuDEXixM3PycniL4iVCLyUyvcl6Lt0mtv3am08sucskpG0tYkW1KlRhTgj4LI5ehyxriVVcfdoxuuP8csi3kQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2459,9 +2529,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-musl": {
|
||||
"version": "15.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.1.4.tgz",
|
||||
"integrity": "sha512-0578bLRVDJOh+LdIoKvgNDz77+Bd85c5JrFgnlbI1SM3WmEQvsjxTA8ATu9Z9FCiIS/AliVAW2DV/BDwpXbtiQ==",
|
||||
"version": "15.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.1.6.tgz",
|
||||
"integrity": "sha512-L4druWmdFSZIIRhF+G60API5sFB7suTbDRhYWSjiw0RbE+15igQvE2g2+S973pMGvwN3guw7cJUjA/TmbPWTHQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2476,9 +2546,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-arm64-msvc": {
|
||||
"version": "15.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.1.4.tgz",
|
||||
"integrity": "sha512-JgFCiV4libQavwII+kncMCl30st0JVxpPOtzWcAI2jtum4HjYaclobKhj+JsRu5tFqMtA5CJIa0MvYyuu9xjjQ==",
|
||||
"version": "15.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.1.6.tgz",
|
||||
"integrity": "sha512-s8w6EeqNmi6gdvM19tqKKWbCyOBvXFbndkGHl+c9YrzsLARRdCHsD9S1fMj8gsXm9v8vhC8s3N8rjuC/XrtkEg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2493,9 +2563,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-x64-msvc": {
|
||||
"version": "15.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.1.4.tgz",
|
||||
"integrity": "sha512-xxsJy9wzq7FR5SqPCUqdgSXiNXrMuidgckBa8nH9HtjjxsilgcN6VgXF6tZ3uEWuVEadotQJI8/9EQ6guTC4Yw==",
|
||||
"version": "15.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.1.6.tgz",
|
||||
"integrity": "sha512-6xomMuu54FAFxttYr5PJbEfu96godcxBTRk1OhAvJq0/EnmFU/Ybiax30Snis4vdWZ9LGpf7Roy5fSs7v/5ROQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -16219,13 +16289,13 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/next": {
|
||||
"version": "15.1.4",
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-15.1.4.tgz",
|
||||
"integrity": "sha512-mTaq9dwaSuwwOrcu3ebjDYObekkxRnXpuVL21zotM8qE2W0HBOdVIdg2Li9QjMEZrj73LN96LcWcz62V19FjAg==",
|
||||
"version": "15.1.6",
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-15.1.6.tgz",
|
||||
"integrity": "sha512-Hch4wzbaX0vKQtalpXvUiw5sYivBy4cm5rzUKrBnUB/y436LGrvOUqYvlSeNVCWFO/770gDlltR9gqZH62ct4Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@next/env": "15.1.4",
|
||||
"@next/env": "15.1.6",
|
||||
"@swc/counter": "0.1.3",
|
||||
"@swc/helpers": "0.5.15",
|
||||
"busboy": "1.6.0",
|
||||
@@ -16240,14 +16310,14 @@
|
||||
"node": "^18.18.0 || ^19.8.0 || >= 20.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@next/swc-darwin-arm64": "15.1.4",
|
||||
"@next/swc-darwin-x64": "15.1.4",
|
||||
"@next/swc-linux-arm64-gnu": "15.1.4",
|
||||
"@next/swc-linux-arm64-musl": "15.1.4",
|
||||
"@next/swc-linux-x64-gnu": "15.1.4",
|
||||
"@next/swc-linux-x64-musl": "15.1.4",
|
||||
"@next/swc-win32-arm64-msvc": "15.1.4",
|
||||
"@next/swc-win32-x64-msvc": "15.1.4",
|
||||
"@next/swc-darwin-arm64": "15.1.6",
|
||||
"@next/swc-darwin-x64": "15.1.6",
|
||||
"@next/swc-linux-arm64-gnu": "15.1.6",
|
||||
"@next/swc-linux-arm64-musl": "15.1.6",
|
||||
"@next/swc-linux-x64-gnu": "15.1.6",
|
||||
"@next/swc-linux-x64-musl": "15.1.6",
|
||||
"@next/swc-win32-arm64-msvc": "15.1.6",
|
||||
"@next/swc-win32-x64-msvc": "15.1.6",
|
||||
"sharp": "^0.33.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@@ -24104,6 +24174,15 @@
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "3.24.1",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz",
|
||||
"integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+6
-2
@@ -14,6 +14,7 @@
|
||||
"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",
|
||||
@@ -25,7 +26,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 watch-client watch-extensions",
|
||||
"watch": "npm-run-all -lp watchreact 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",
|
||||
@@ -78,9 +79,11 @@
|
||||
},
|
||||
"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",
|
||||
@@ -132,7 +135,8 @@
|
||||
"vscode-regexpp": "^3.1.0",
|
||||
"vscode-textmate": "9.1.0",
|
||||
"yauzl": "^3.0.0",
|
||||
"yazl": "^2.4.3"
|
||||
"yazl": "^2.4.3",
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.46.1",
|
||||
|
||||
Generated
+7
@@ -29,6 +29,7 @@
|
||||
"@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",
|
||||
@@ -396,6 +397,12 @@
|
||||
"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",
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
"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",
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 173 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 5.5 MiB After Width: | Height: | Size: 1.4 MiB |
@@ -121,11 +121,11 @@ 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 '../../platform/void/electron-main/llmMessageChannel.js';
|
||||
import { IMetricsService } from '../../platform/void/common/metricsService.js';
|
||||
import { MetricsMainService } from '../../platform/void/electron-main/metricsMainService.js';
|
||||
import { VoidMainUpdateService } from '../../platform/void/electron-main/voidUpdateMainService.js';
|
||||
import { IVoidUpdateService } from '../../platform/void/common/voidUpdateService.js';
|
||||
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';
|
||||
/**
|
||||
* 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).
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
// ---------- common ----------
|
||||
|
||||
// llmMessage
|
||||
import '../common/llmMessageService.js'
|
||||
|
||||
// voidSettings
|
||||
import '../common/voidSettingsService.js'
|
||||
|
||||
// refreshModel
|
||||
import '../common/refreshModelService.js'
|
||||
|
||||
// metrics
|
||||
import '../common/metricsService.js'
|
||||
|
||||
// updates
|
||||
import '../common/voidUpdateService.js'
|
||||
@@ -1,68 +0,0 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
// // Greptile
|
||||
// // https://docs.greptile.com/api-reference/query
|
||||
// // 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,105 +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 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 });
|
||||
}
|
||||
})
|
||||
|
||||
};
|
||||
@@ -1,114 +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 { 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,
|
||||
modelName,
|
||||
numMessages: messages?.length,
|
||||
messagesShape: messages?.map(msg => ({ role: msg.role, length: msg.content.length })),
|
||||
...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.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,100 +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 { isLinux, isMacintosh, isWindows } from '../../../base/common/platform.js';
|
||||
import { generateUuid } from '../../../base/common/uuid.js';
|
||||
import { IEnvironmentMainService } from '../../environment/electron-main/environmentMainService.js';
|
||||
|
||||
import { IProductService } from '../../product/common/productService.js';
|
||||
import { IStorageMainService } from '../../storage/electron-main/storageMainService.js';
|
||||
|
||||
import { IMetricsService } from '../common/metricsService.js';
|
||||
import { PostHog } from 'posthog-node'
|
||||
|
||||
|
||||
const os = isWindows ? 'windows' : isMacintosh ? 'mac' : isLinux ? 'linux' : null
|
||||
|
||||
const VOID_MACHINE_STORAGE_KEY = 'void.machineId'
|
||||
|
||||
export class MetricsMainService extends Disposable implements IMetricsService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
private readonly client: PostHog
|
||||
|
||||
private readonly _initProperties: object
|
||||
|
||||
|
||||
// TODO we should eventually identify people based on email
|
||||
private get machineId() {
|
||||
const currVal = this._storageService.applicationStorage.get(VOID_MACHINE_STORAGE_KEY)
|
||||
if (currVal !== undefined) return currVal
|
||||
const newVal = generateUuid()
|
||||
this._storageService.applicationStorage.set(VOID_MACHINE_STORAGE_KEY, newVal)
|
||||
return newVal
|
||||
}
|
||||
|
||||
|
||||
constructor(
|
||||
@IProductService private readonly _productService: IProductService,
|
||||
@IStorageMainService private readonly _storageService: IStorageMainService,
|
||||
@IEnvironmentMainService private readonly _envMainService: IEnvironmentMainService,
|
||||
) {
|
||||
super()
|
||||
this.client = new PostHog('phc_UanIdujHiLp55BkUTjB1AuBXcasVkdqRwgnwRlWESH2', {
|
||||
host: 'https://us.i.posthog.com',
|
||||
})
|
||||
|
||||
// we'd like to use devDeviceId on telemetryService, but that gets sanitized by the time it gets here as 'someValue.devDeviceId'
|
||||
|
||||
const { commit, version, quality } = this._productService
|
||||
|
||||
const isDevMode = !this._envMainService.isBuilt // found in abstractUpdateService.ts
|
||||
|
||||
|
||||
// custom properties we identify
|
||||
this._initProperties = {
|
||||
commit,
|
||||
version,
|
||||
os,
|
||||
quality,
|
||||
distinctId: this.machineId,
|
||||
isDevMode,
|
||||
...this._getOSInfo(),
|
||||
}
|
||||
|
||||
const identifyMessage = {
|
||||
distinctId: this.machineId,
|
||||
properties: this._initProperties,
|
||||
}
|
||||
this.client.identify(identifyMessage)
|
||||
|
||||
console.log('Void posthog metrics info:', JSON.stringify(identifyMessage, null, 2))
|
||||
|
||||
}
|
||||
|
||||
_getOSInfo() {
|
||||
try {
|
||||
const { platform, arch } = process // see platform.ts
|
||||
return { platform, arch }
|
||||
}
|
||||
catch (e) {
|
||||
return { osInfo: { platform: '??', arch: '??' } }
|
||||
}
|
||||
}
|
||||
|
||||
capture: IMetricsService['capture'] = (event, params) => {
|
||||
const capture = { distinctId: this.machineId, event, properties: params } as const
|
||||
// console.log('full capture:', capture)
|
||||
this.client.capture(capture)
|
||||
}
|
||||
|
||||
|
||||
async getDebuggingProperties() {
|
||||
return this._initProperties
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ 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 {
|
||||
|
||||
|
||||
@@ -432,7 +432,7 @@ export class UnpinEditorAction extends Action {
|
||||
label: string,
|
||||
@ICommandService private readonly commandService: ICommandService
|
||||
) {
|
||||
super(id, label, ThemeIcon.asClassName(Codicon.pinned));
|
||||
super(id, label, ThemeIcon.asClassName(Codicon.starFull));
|
||||
}
|
||||
|
||||
override run(context?: IEditorCommandsContext): Promise<void> {
|
||||
@@ -440,6 +440,24 @@ 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';
|
||||
|
||||
@@ -146,25 +146,6 @@ 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'));
|
||||
@@ -176,6 +157,10 @@ export class EditorGroupWatermark extends Disposable {
|
||||
|
||||
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);
|
||||
|
||||
@@ -206,10 +191,6 @@ export class EditorGroupWatermark extends Disposable {
|
||||
|
||||
|
||||
// Recents
|
||||
const recentlyOpened = await this.workspacesService.getRecentlyOpened()
|
||||
.catch(() => ({ files: [], workspaces: [] })).then(w => w.workspaces);
|
||||
|
||||
|
||||
if (recentlyOpened.length !== 0) {
|
||||
|
||||
voidIconBox.append(
|
||||
|
||||
@@ -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: hidden; /* let the tab actions be pushed out of view when sizing is set to shrink/fixed to make more room */
|
||||
overflow: visible; /* ensure tab actions are always visible */
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.dirty.tab-actions-right.sizing-shrink > .tab-actions,
|
||||
@@ -399,18 +399,8 @@
|
||||
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; /* 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;
|
||||
display: none; /* only hide tab actions when sticky-compact */
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab > .tab-actions .actions-container {
|
||||
@@ -444,11 +434,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: 0.5; /* show tab actions dimmed for inactive group */
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab > .tab-actions .action-label {
|
||||
opacity: 0;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* 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, UnpinEditorAction } from './editorActions.js';
|
||||
import { CloseEditorTabAction, PinEditorAction, 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,6 +113,7 @@ 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)); // Add this line
|
||||
|
||||
private readonly tabResourceLabels = this._register(this.instantiationService.createInstance(ResourceLabels, DEFAULT_LABELS_CONTAINER));
|
||||
private tabLabels: IEditorInputLabel[] = [];
|
||||
@@ -1518,28 +1519,28 @@ export class MultiEditorTabsControl extends EditorTabsControl {
|
||||
this.redrawTabLabel(editor, tabIndex, tabContainer, tabLabelWidget, tabLabel);
|
||||
|
||||
// Action
|
||||
const hasUnpinAction = isTabSticky && options.tabActionUnpinVisibility;
|
||||
const hasCloseAction = !hasUnpinAction && options.tabActionCloseVisibility;
|
||||
const hasAction = hasUnpinAction || hasCloseAction;
|
||||
const hasCloseAction = options.tabActionCloseVisibility;
|
||||
const hasAction = true; // Always show actions
|
||||
|
||||
// Determine which action to show
|
||||
let tabAction;
|
||||
if (hasAction) {
|
||||
tabAction = hasUnpinAction ? this.unpinEditorAction : this.closeEditorAction;
|
||||
if (isTabSticky) {
|
||||
tabAction = this.unpinEditorAction;
|
||||
} else {
|
||||
// Even if the action is not visible, add it as it contains the dirty indicator
|
||||
tabAction = isTabSticky ? this.unpinEditorAction : this.closeEditorAction;
|
||||
tabAction = this.pinEditorAction; // Use pin action instead of close action
|
||||
}
|
||||
|
||||
// Update action bar
|
||||
if (!tabActionBar.hasAction(tabAction)) {
|
||||
if (!tabActionBar.isEmpty()) {
|
||||
tabActionBar.clear();
|
||||
}
|
||||
|
||||
tabActionBar.push(tabAction, { icon: true, label: false, keybinding: this.getKeybindingLabel(tabAction) });
|
||||
}
|
||||
|
||||
tabContainer.classList.toggle(`pinned-action-off`, isTabSticky && !hasUnpinAction);
|
||||
tabContainer.classList.toggle(`close-action-off`, !hasUnpinAction && !hasCloseAction);
|
||||
tabContainer.classList.toggle('sticky', isTabSticky);
|
||||
tabContainer.classList.toggle(`pinned-action-off`, false);
|
||||
tabContainer.classList.toggle(`close-action-off`, !hasCloseAction);
|
||||
|
||||
for (const option of ['left', 'right']) {
|
||||
tabContainer.classList.toggle(`tab-actions-${option}`, hasAction && options.tabActionLocation === option);
|
||||
|
||||
@@ -289,6 +289,7 @@ export interface IFileTemplateData {
|
||||
readonly templateDisposables: DisposableStore;
|
||||
readonly elementDisposables: DisposableStore;
|
||||
readonly label: IResourceLabel;
|
||||
// readonly voidLabels: IResourceLabel;
|
||||
readonly container: HTMLElement;
|
||||
readonly contribs: IExplorerFileContribution[];
|
||||
currentContext?: ExplorerItem;
|
||||
@@ -347,15 +348,25 @@ 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);
|
||||
@@ -365,10 +376,15 @@ 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;
|
||||
@@ -382,8 +398,7 @@ export class FilesRenderer implements ICompressibleTreeRenderer<ExplorerItem, Fu
|
||||
templateData.label.element.style.display = 'flex';
|
||||
this.renderStat(stat, stat.name, undefined, node.filterData, templateData);
|
||||
}
|
||||
|
||||
// Input Box
|
||||
// Input Box (Void - shown only if currently editing - this is the box that appears when user edits the name of the file)
|
||||
else {
|
||||
templateData.label.element.style.display = 'none';
|
||||
templateData.contribs.forEach(c => c.setResource(undefined));
|
||||
@@ -477,6 +492,13 @@ 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,7 +95,12 @@ suite('Files - ExplorerView', () => {
|
||||
label: <any>{
|
||||
container: label,
|
||||
onDidRender: emitter.event
|
||||
}
|
||||
},
|
||||
// voidLabels: <any>{
|
||||
// container: label,
|
||||
// onDidRender: emitter.event
|
||||
// },
|
||||
|
||||
}, 1, false);
|
||||
|
||||
ds.add(navigationController);
|
||||
|
||||
@@ -5,19 +5,21 @@
|
||||
|
||||
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';
|
||||
|
||||
// The extension this was called from is here - https://github.com/voideditor/void/blob/autocomplete/extensions/void/src/extension/extension.ts
|
||||
|
||||
@@ -134,17 +136,27 @@ class LRUCache<K, V> {
|
||||
}
|
||||
}
|
||||
|
||||
type AutocompletionStatus = 'pending' | 'finished' | 'error';
|
||||
type AutocompletionPredictionType =
|
||||
| 'single-line-fill-middle'
|
||||
| 'single-line-redo-suffix'
|
||||
// | 'multi-line-start-here'
|
||||
| 'multi-line-start-on-next-line'
|
||||
| 'do-not-predict'
|
||||
|
||||
type Autocompletion = {
|
||||
id: number,
|
||||
prefix: string,
|
||||
suffix: string,
|
||||
llmPrefix: string,
|
||||
llmSuffix: string,
|
||||
startTime: number,
|
||||
endTime: number | undefined,
|
||||
status: AutocompletionStatus,
|
||||
status: 'pending' | 'finished' | 'error',
|
||||
type: AutocompletionPredictionType,
|
||||
llmPromise: Promise<string> | undefined,
|
||||
insertText: string,
|
||||
requestId: string | null,
|
||||
_newlineCount: number,
|
||||
}
|
||||
|
||||
const DEBOUNCE_TIME = 500
|
||||
@@ -153,13 +165,16 @@ const MAX_CACHE_SIZE = 20
|
||||
const MAX_PENDING_REQUESTS = 2
|
||||
|
||||
// postprocesses the result
|
||||
const postprocessResult = (result: string) => {
|
||||
const processStartAndEndSpaces = (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 ? ' ' : '');
|
||||
@@ -168,13 +183,13 @@ const postprocessResult = (result: string) => {
|
||||
|
||||
|
||||
// trims the end of the prefix to improve cache hit rate
|
||||
const removeLeftTabsAndTrimEnd = (s: string): string => {
|
||||
const removeLeftTabsAndTrimEnds = (s: string): string => {
|
||||
const trimmedString = s.trimEnd();
|
||||
const trailingEnd = s.slice(trimmedString.length);
|
||||
|
||||
// keep only a single trailing newline
|
||||
if (trailingEnd.includes('\n')) {
|
||||
s = trimmedString + '\n';
|
||||
if (trailingEnd.includes(_ln)) {
|
||||
s = trimmedString + _ln;
|
||||
}
|
||||
|
||||
s = s.replace(/^\s+/gm, ''); // remove left tabs
|
||||
@@ -184,7 +199,32 @@ const removeLeftTabsAndTrimEnd = (s: string): string => {
|
||||
|
||||
|
||||
|
||||
function getStringUpToUnbalancedParenthesis(s: string, prefix: 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 {
|
||||
|
||||
const pairs: Record<string, string> = { ')': '(', '}': '{', ']': '[' };
|
||||
|
||||
@@ -220,19 +260,14 @@ function getStringUpToUnbalancedParenthesis(s: string, prefix: string): string {
|
||||
}
|
||||
|
||||
|
||||
const parenthesisChars = `{}()[]<>\`'"`
|
||||
// further trim the autocompletion
|
||||
const postprocessAutocompletion = ({ autocompletionMatchup, autocompletion, prefixAndSuffix }: { autocompletionMatchup: AutocompletionMatchupBounds, autocompletion: Autocompletion, prefixAndSuffix: PrefixAndSuffixInfo }) => {
|
||||
|
||||
// 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 { prefix, prefixToTheLeftOfCursor, suffixToTheRightOfCursor } = prefixAndSuffix
|
||||
|
||||
|
||||
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 = matchInfo.startIdx
|
||||
let startIdx = autocompletionMatchup.startIdx
|
||||
let endIdx = generatedMiddle.length // exclusive bounds
|
||||
|
||||
// const naiveReturnValue = generatedMiddle.slice(startIdx)
|
||||
@@ -253,7 +288,7 @@ const toInlineCompletions = ({ matchInfo, prefix, suffix, autocompletion, positi
|
||||
}
|
||||
|
||||
// if user is on a blank line and the generation starts with newline(s), remove them
|
||||
const numStartingNewlines = generatedMiddle.slice(startIdx).match(/^\n+/)?.[0].length || 0;
|
||||
const numStartingNewlines = generatedMiddle.slice(startIdx).match(new RegExp(`^${_ln}+`))?.[0].length || 0;
|
||||
if (
|
||||
!prefixToTheLeftOfCursor.trim()
|
||||
&& !suffixToTheRightOfCursor.trim()
|
||||
@@ -263,21 +298,21 @@ const toInlineCompletions = ({ matchInfo, prefix, suffix, autocompletion, positi
|
||||
startIdx += numStartingNewlines
|
||||
}
|
||||
|
||||
// if the generated text matches with the suffix on the current line, stop
|
||||
if (suffixToTheRightOfCursor.trim()) { // completing in the middle of a line
|
||||
// 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
|
||||
// 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 (parenthesisChars.includes(matchChar)) {
|
||||
if (`{}()[]<>\`'"`.includes(matchChar)) {
|
||||
endIdx = Math.min(endIdx, matchIdx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const restOfLineToGenerate = generatedMiddle.slice(startIdx).split('\n')[0] ?? ''
|
||||
const restOfLineToGenerate = generatedMiddle.slice(startIdx).split(_ln)[0] ?? ''
|
||||
// condition to complete as a single line completion
|
||||
if (
|
||||
prefixToTheLeftOfCursor.trim()
|
||||
@@ -285,7 +320,7 @@ const toInlineCompletions = ({ matchInfo, prefix, suffix, autocompletion, positi
|
||||
&& restOfLineToGenerate.trim()
|
||||
) {
|
||||
|
||||
const rawNewlineIdx = generatedMiddle.slice(startIdx).indexOf('\n')
|
||||
const rawNewlineIdx = generatedMiddle.slice(startIdx).indexOf(_ln)
|
||||
if (rawNewlineIdx > -1) {
|
||||
// console.log('p3', startIdx, rawNewlineIdx)
|
||||
const newlineIdx = rawNewlineIdx + startIdx;
|
||||
@@ -311,14 +346,48 @@ const toInlineCompletions = ({ matchInfo, prefix, suffix, autocompletion, positi
|
||||
let completionStr = generatedMiddle.slice(startIdx, endIdx)
|
||||
|
||||
// filter out unbalanced parentheses
|
||||
completionStr = getStringUpToUnbalancedParenthesis(completionStr, prefix)
|
||||
completionStr = getStringUpToUnbalancedClosingParenthesis(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: completionStr,
|
||||
insertText: trimmedInsertText,
|
||||
range: rangeToReplace,
|
||||
}]
|
||||
|
||||
@@ -345,7 +414,12 @@ const toInlineCompletions = ({ matchInfo, prefix, suffix, autocompletion, positi
|
||||
|
||||
// }
|
||||
|
||||
const getPrefixAndSuffix = (model: ITextModel, position: Position) => {
|
||||
|
||||
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 fullText = model.getValue();
|
||||
|
||||
@@ -353,30 +427,37 @@ const getPrefixAndSuffix = (model: ITextModel, position: Position) => {
|
||||
const prefix = fullText.substring(0, cursorOffset)
|
||||
const suffix = fullText.substring(cursorOffset)
|
||||
|
||||
return { prefix, suffix }
|
||||
|
||||
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 }
|
||||
|
||||
}
|
||||
|
||||
const getIndex = (str: string, line: number, char: number) => {
|
||||
return str.split('\n').slice(0, line).join('\n').length + (line > 0 ? 1 : 0) + char;
|
||||
return str.split(_ln).slice(0, line).join(_ln).length + (line > 0 ? 1 : 0) + char;
|
||||
}
|
||||
const getLastLine = (s: string): string => {
|
||||
const matches = s.match(/[^\n]*$/)
|
||||
const matches = s.match(new RegExp(`[^${_ln}]*$`))
|
||||
return matches ? matches[0] : ''
|
||||
}
|
||||
|
||||
type matchInfo = {
|
||||
lineStart: number,
|
||||
character: number,
|
||||
type AutocompletionMatchupBounds = {
|
||||
startLine: number,
|
||||
startCharacter: 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 getPrefixAutocompletionMatch = ({ prefix, autocompletion }: { prefix: string, autocompletion: Autocompletion }): matchInfo | undefined => {
|
||||
const getAutocompletionMatchup = ({ prefix, autocompletion }: { prefix: string, autocompletion: Autocompletion }): AutocompletionMatchupBounds | undefined => {
|
||||
|
||||
const trimmedCurrentPrefix = removeLeftTabsAndTrimEnd(prefix)
|
||||
const trimmedCompletionPrefix = removeLeftTabsAndTrimEnd(autocompletion.prefix)
|
||||
const trimmedCompletionMiddle = removeLeftTabsAndTrimEnd(autocompletion.insertText)
|
||||
const trimmedCurrentPrefix = removeLeftTabsAndTrimEnds(prefix)
|
||||
const trimmedCompletionPrefix = removeLeftTabsAndTrimEnds(autocompletion.prefix)
|
||||
const trimmedCompletionMiddle = removeLeftTabsAndTrimEnds(autocompletion.insertText)
|
||||
|
||||
// console.log('@result: ', JSON.stringify(autocompletion.insertText))
|
||||
// console.log('@trimmedCurrentPrefix: ', JSON.stringify(trimmedCurrentPrefix))
|
||||
@@ -384,7 +465,7 @@ const getPrefixAutocompletionMatch = ({ prefix, autocompletion }: { prefix: stri
|
||||
// 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
|
||||
}
|
||||
|
||||
@@ -392,24 +473,24 @@ const getPrefixAutocompletionMatch = ({ prefix, autocompletion }: { prefix: stri
|
||||
!(trimmedCompletionPrefix + trimmedCompletionMiddle)
|
||||
.startsWith(trimmedCurrentPrefix)
|
||||
) {
|
||||
console.log('@undefined2')
|
||||
// console.log('@undefined2')
|
||||
return undefined
|
||||
}
|
||||
|
||||
// reverse map to find position wrt `autocompletion.result`
|
||||
const lineStart =
|
||||
trimmedCurrentPrefix.split('\n').length -
|
||||
trimmedCompletionPrefix.split('\n').length;
|
||||
trimmedCurrentPrefix.split(_ln).length -
|
||||
trimmedCompletionPrefix.split(_ln).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('\n')[lineStart]
|
||||
const completionMiddleLine = autocompletion.insertText.split(_ln)[lineStart]
|
||||
const fullCompletionLine = completionPrefixLine + completionMiddleLine
|
||||
|
||||
// console.log('currentPrefixLine', currentPrefixLine)
|
||||
@@ -418,7 +499,7 @@ const getPrefixAutocompletionMatch = ({ prefix, autocompletion }: { prefix: stri
|
||||
|
||||
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
|
||||
@@ -432,8 +513,8 @@ const getPrefixAutocompletionMatch = ({ prefix, autocompletion }: { prefix: stri
|
||||
const startIdx = getIndex(autocompletion.insertText, lineStart, character)
|
||||
|
||||
return {
|
||||
lineStart,
|
||||
character,
|
||||
startLine: lineStart,
|
||||
startCharacter: character,
|
||||
startIdx,
|
||||
}
|
||||
|
||||
@@ -441,40 +522,89 @@ const getPrefixAutocompletionMatch = ({ prefix, autocompletion }: { prefix: stri
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
|
||||
const getCompletionOptions = ({ prefix, suffix }: { prefix: string, suffix: string }) => {
|
||||
// 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 prefixLines = prefix.split('\n')
|
||||
const suffixLines = suffix.split('\n')
|
||||
let completionOptions: CompletionOptions
|
||||
|
||||
const prefixToLeftOfCursor = prefixLines.slice(-1)[0] ?? ''
|
||||
const suffixToRightOfCursor = suffixLines[0] ?? ''
|
||||
// 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
|
||||
|
||||
// default parameters
|
||||
let shouldGenerate = true
|
||||
let stopTokens: string[] = ['\n\n', '\r\n\r\n']
|
||||
// TODO add context to prefix
|
||||
// llmPrefix = '\n\n/* Relevant context:\n' + relevantContext + '\n*/\n' + llmPrefix
|
||||
|
||||
// specific cases
|
||||
if (suffixToRightOfCursor.trim() !== '') { // typing between something
|
||||
stopTokens = ['\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: []
|
||||
}
|
||||
}
|
||||
|
||||
// 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 }
|
||||
return completionOptions
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
export interface IAutocompleteService {
|
||||
readonly _serviceBrand: undefined;
|
||||
}
|
||||
@@ -482,13 +612,17 @@ 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 _lastCompletionTime = 0
|
||||
private _lastPrefix: string = ''
|
||||
private _lastCompletionStart = 0
|
||||
private _lastCompletionAccept = 0
|
||||
// private _lastPrefix: string = ''
|
||||
|
||||
// used internally by vscode
|
||||
// fires after every keystroke and returns the completion to show
|
||||
@@ -499,16 +633,15 @@ 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 { prefix, suffix } = getPrefixAndSuffix(model, position)
|
||||
// initialize cache and other variables
|
||||
// note that whenever an autocompletion is rejected, it is removed from cache
|
||||
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
|
||||
if (!this._autocompletionsOfDocument[docUriStr]) {
|
||||
this._autocompletionsOfDocument[docUriStr] = new LRUCache<number, Autocompletion>(
|
||||
MAX_CACHE_SIZE,
|
||||
@@ -518,7 +651,7 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
|
||||
}
|
||||
)
|
||||
}
|
||||
this._lastPrefix = prefix
|
||||
// this._lastPrefix = prefix
|
||||
|
||||
// print all pending autocompletions
|
||||
// let _numPending = 0
|
||||
@@ -527,33 +660,36 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
|
||||
|
||||
// get autocompletion from cache
|
||||
let cachedAutocompletion: Autocompletion | undefined = undefined
|
||||
let matchInfo: matchInfo | undefined = undefined
|
||||
let autocompletionMatchup: AutocompletionMatchupBounds | undefined = undefined
|
||||
for (const autocompletion of this._autocompletionsOfDocument[docUriStr].items.values()) {
|
||||
// if the user's change matches up with the generated text
|
||||
matchInfo = getPrefixAutocompletionMatch({ prefix, autocompletion })
|
||||
if (matchInfo !== undefined) {
|
||||
// if the user's change matches with the autocompletion
|
||||
autocompletionMatchup = getAutocompletionMatchup({ prefix, autocompletion })
|
||||
if (autocompletionMatchup !== undefined) {
|
||||
cachedAutocompletion = autocompletion
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// if there is a cached autocompletion, return it
|
||||
if (cachedAutocompletion && matchInfo) {
|
||||
if (cachedAutocompletion && autocompletionMatchup) {
|
||||
|
||||
console.log('AA')
|
||||
|
||||
|
||||
// console.log('id: ' + cachedAutocompletion.id)
|
||||
|
||||
if (cachedAutocompletion.status === 'finished') {
|
||||
// console.log('A1')
|
||||
console.log('A1')
|
||||
|
||||
const inlineCompletions = toInlineCompletions({ matchInfo, autocompletion: cachedAutocompletion, prefix, suffix, position, debug: true })
|
||||
const inlineCompletions = toInlineCompletions({ autocompletionMatchup, autocompletion: cachedAutocompletion, prefixAndSuffix, position, debug: true })
|
||||
return inlineCompletions
|
||||
|
||||
} else if (cachedAutocompletion.status === 'pending') {
|
||||
// console.log('A2')
|
||||
console.log('A2')
|
||||
|
||||
try {
|
||||
await cachedAutocompletion.llmPromise;
|
||||
const inlineCompletions = toInlineCompletions({ matchInfo, autocompletion: cachedAutocompletion, prefix, suffix, position })
|
||||
const inlineCompletions = toInlineCompletions({ autocompletionMatchup, autocompletion: cachedAutocompletion, prefixAndSuffix, position })
|
||||
return inlineCompletions
|
||||
|
||||
} catch (e) {
|
||||
@@ -562,19 +698,25 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
|
||||
}
|
||||
|
||||
} else if (cachedAutocompletion.status === 'error') {
|
||||
// console.log('A3')
|
||||
console.log('A3')
|
||||
} else {
|
||||
console.log('A4')
|
||||
}
|
||||
|
||||
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()
|
||||
this._lastCompletionTime = thisTime
|
||||
|
||||
const justAcceptedAutocompletion = thisTime - this._lastCompletionAccept < 500
|
||||
|
||||
this._lastCompletionStart = thisTime
|
||||
const didTypingHappenDuringDebounce = await new Promise((resolve, reject) =>
|
||||
setTimeout(() => {
|
||||
if (this._lastCompletionTime === thisTime) {
|
||||
if (this._lastCompletionStart === thisTime) {
|
||||
resolve(false)
|
||||
} else {
|
||||
resolve(true)
|
||||
@@ -605,7 +747,15 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
|
||||
}
|
||||
}
|
||||
|
||||
const { shouldGenerate, stopTokens: _ } = getCompletionOptions({ prefix, suffix }) // TODO mat
|
||||
|
||||
// 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)
|
||||
|
||||
if (!shouldGenerate) return []
|
||||
|
||||
@@ -613,47 +763,74 @@ 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,
|
||||
prefix: prefix, // the actual prefix and suffix
|
||||
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' },
|
||||
messages: [],
|
||||
onText: async ({ newText, fullText }) => {
|
||||
onText: async ({ fullText, newText }) => {
|
||||
|
||||
newAutocompletion.insertText = fullText
|
||||
|
||||
// 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.')
|
||||
// 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 (!getAutocompletionMatchup({ prefix: this._lastPrefix, autocompletion: newAutocompletion })) {
|
||||
// reject('LLM response did not match user\'s text.')
|
||||
// }
|
||||
},
|
||||
onFinalMessage: ({ fullText }) => {
|
||||
|
||||
// newAutocompletion.prefix = prefix
|
||||
// newAutocompletion.suffix = suffix
|
||||
// newAutocompletion.startTime = Date.now()
|
||||
// console.log('____res: ', JSON.stringify(newAutocompletion.insertText))
|
||||
|
||||
newAutocompletion.endTime = Date.now()
|
||||
// newAutocompletion.abortRef = { current: () => { } }
|
||||
newAutocompletion.status = 'finished'
|
||||
// newAutocompletion.promise = undefined
|
||||
const [text, _] = extractCodeFromRegular({ text: fullText, recentlyAddedTextLen: 0 })
|
||||
newAutocompletion.insertText = postprocessResult(text)
|
||||
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
|
||||
}
|
||||
|
||||
resolve(newAutocompletion.insertText)
|
||||
|
||||
@@ -663,8 +840,6 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
|
||||
newAutocompletion.status = 'error'
|
||||
reject(message)
|
||||
},
|
||||
useProviderFor: 'Autocomplete',
|
||||
range: { startLineNumber: position.lineNumber, startColumn: position.column, endLineNumber: position.lineNumber, endColumn: position.column },
|
||||
})
|
||||
newAutocompletion.requestId = requestId
|
||||
|
||||
@@ -687,8 +862,8 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
|
||||
await newAutocompletion.llmPromise
|
||||
// console.log('id: ' + newAutocompletion.id)
|
||||
|
||||
const matchInfo: matchInfo = { startIdx: 0, lineStart: 0, character: 0 }
|
||||
const inlineCompletions = toInlineCompletions({ matchInfo, autocompletion: newAutocompletion, prefix, suffix, position })
|
||||
const autocompletionMatchup: AutocompletionMatchupBounds = { startIdx: 0, startLine: 0, startCharacter: 0 }
|
||||
const inlineCompletions = toInlineCompletions({ autocompletionMatchup, autocompletion: newAutocompletion, prefixAndSuffix, position })
|
||||
return inlineCompletions
|
||||
|
||||
} catch (e) {
|
||||
@@ -704,6 +879,7 @@ 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()
|
||||
|
||||
@@ -715,7 +891,6 @@ 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;
|
||||
@@ -728,33 +903,32 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
|
||||
const model = this._modelService.getModel(resource)
|
||||
if (!model) return;
|
||||
const docUriStr = resource.toString();
|
||||
|
||||
const { prefix, } = getPrefixAndSuffix(model, position)
|
||||
|
||||
if (!this._autocompletionsOfDocument[docUriStr]) return;
|
||||
|
||||
const { prefix, } = getPrefixAndSuffixInfo(model, position)
|
||||
|
||||
// go through cached items and remove matching ones
|
||||
// autocompletion.prefix + autocompletion.insertedText ~== insertedText
|
||||
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);
|
||||
}
|
||||
});
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
registerSingleton(IAutocompleteService, AutocompleteService, InstantiationType.Eager);
|
||||
registerWorkbenchContribution2(AutocompleteService.ID, AutocompleteService, WorkbenchPhase.BlockRestore);
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -11,39 +11,40 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../platfo
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
import { Emitter, Event } from '../../../../base/common/event.js';
|
||||
import { IRange } from '../../../../editor/common/core/range.js';
|
||||
import { ILLMMessageService } from '../../../../platform/void/common/llmMessageService.js';
|
||||
import { ILLMMessageService } from '../common/llmMessageService.js';
|
||||
import { IModelService } from '../../../../editor/common/services/model.js';
|
||||
import { VSReadFile } from './helpers/readFile.js';
|
||||
import { chat_prompt, chat_systemMessage } from './prompt/prompts.js';
|
||||
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 | null;
|
||||
content: string; // TODO remove this (replace `selectionStr` with `content`)
|
||||
selectionStr: string;
|
||||
range: IRange;
|
||||
}
|
||||
|
||||
// if selectionStr is null, it means to use the entire file at send time
|
||||
export type CodeStagingSelection = {
|
||||
type: 'Selection',
|
||||
fileURI: URI,
|
||||
selectionStr: string,
|
||||
range: IRange
|
||||
} | {
|
||||
type: 'File',
|
||||
fileURI: URI,
|
||||
selectionStr: null,
|
||||
range: null
|
||||
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 sent to the llm - allowed to be '', will be replaced with (empty)
|
||||
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: CodeSelection[] | null; // the user's selection
|
||||
selections: StagingSelectionItem[] | null; // the user's selection
|
||||
state: {
|
||||
stagingSelections: StagingSelectionItem[];
|
||||
isBeingEdited: boolean;
|
||||
}
|
||||
}
|
||||
| {
|
||||
role: 'assistant';
|
||||
@@ -56,6 +57,11 @@ export type ChatMessage =
|
||||
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]: {
|
||||
@@ -63,18 +69,26 @@ export type ChatThreads = {
|
||||
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
|
||||
currentStagingSelections: CodeStagingSelection[] | null;
|
||||
}
|
||||
|
||||
export type ThreadStreamState = {
|
||||
[threadId: string]: undefined | {
|
||||
error?: { message: string, fullError: Error | null };
|
||||
error?: { message: string, fullError: Error | null, };
|
||||
messageSoFar?: string;
|
||||
streamingToken?: string;
|
||||
}
|
||||
@@ -88,9 +102,18 @@ const newThreadObject = () => {
|
||||
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 {
|
||||
@@ -106,8 +129,15 @@ export interface IChatThreadService {
|
||||
openNewThread(): void;
|
||||
switchToThread(threadId: string): void;
|
||||
|
||||
setStaging(stagingSelection: CodeStagingSelection[] | null): 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;
|
||||
@@ -131,6 +161,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
constructor(
|
||||
@IStorageService private readonly _storageService: IStorageService,
|
||||
@IModelService private readonly _modelService: IModelService,
|
||||
@IFileService private readonly _fileService: IFileService,
|
||||
@ILLMMessageService private readonly _llmMessageService: ILLMMessageService,
|
||||
) {
|
||||
super()
|
||||
@@ -138,18 +169,75 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
this.state = {
|
||||
allThreads: this._readAllThreads(),
|
||||
currentThreadId: null as unknown as string, // gets set in startNewThread()
|
||||
currentStagingSelections: null,
|
||||
}
|
||||
|
||||
// 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
|
||||
const threads = this._storageService.get(THREAD_STORAGE_KEY, StorageScope.APPLICATION)
|
||||
return threads ? JSON.parse(threads) : {}
|
||||
// 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) {
|
||||
@@ -166,6 +254,17 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
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],
|
||||
@@ -184,29 +283,71 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
this._setStreamState(threadId, { messageSoFar: undefined, streamingToken: undefined, error })
|
||||
}
|
||||
|
||||
async addUserMessageAndStreamResponse(userMessage: string) {
|
||||
const threadId = this.getCurrentThread().id
|
||||
|
||||
const currSelns = this.state.currentStagingSelections ?? []
|
||||
const selections = !currSelns ? null : await Promise.all(
|
||||
currSelns.map(async (sel) => ({ ...sel, content: await VSReadFile(this._modelService, sel.fileURI) }))
|
||||
).then(
|
||||
(files) => files.filter(file => file.content !== null) as CodeSelection[]
|
||||
)
|
||||
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 userHistoryElt: ChatMessage = { role: 'user', content: chat_prompt(instructions, selections), displayContent: instructions, selections: selections }
|
||||
this._addMessageToThread(threadId, userHistoryElt)
|
||||
|
||||
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 },
|
||||
...this.getCurrentThread().messages.map(m => ({ role: m.role, content: m.content || '(null)' })),
|
||||
...prevLLMMessages,
|
||||
currLLMMessage,
|
||||
],
|
||||
onText: ({ newText, fullText }) => {
|
||||
this._setStreamState(threadId, { messageSoFar: fullText })
|
||||
@@ -217,7 +358,6 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
onError: (error) => {
|
||||
this.finishStreaming(threadId, this.streamState[threadId]?.messageSoFar ?? '', error)
|
||||
},
|
||||
useProviderFor: 'Ctrl+L',
|
||||
|
||||
})
|
||||
if (llmCancelToken === null) return
|
||||
@@ -241,12 +381,29 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
|
||||
getCurrentThread(): ChatThreads[string] {
|
||||
const state = this.state
|
||||
return state.allThreads[state.currentThreadId];
|
||||
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) {
|
||||
// console.log('threadId', threadId)
|
||||
// console.log('messages', this.state.allThreads[threadId].messages)
|
||||
this._setState({ currentThreadId: threadId }, true)
|
||||
}
|
||||
|
||||
@@ -291,11 +448,104 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
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) {
|
||||
|
||||
setStaging(stagingSelection: CodeStagingSelection[] | null): void {
|
||||
this._setState({ currentStagingSelections: stagingSelection }, true) // this is a hack for now
|
||||
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);
|
||||
|
||||
@@ -0,0 +1,354 @@
|
||||
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);
|
||||
@@ -91,10 +91,7 @@ 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) }))
|
||||
}
|
||||
|
||||
|
||||
@@ -127,8 +124,6 @@ export class ConsistentItemService extends Disposable {
|
||||
|
||||
const editorId = editor.getId()
|
||||
this.itemIdsOfEditorId[editorId]?.delete(itemId)
|
||||
if (this.itemIdsOfEditorId[editorId]?.size === 0)
|
||||
delete this.itemIdsOfEditorId[editorId]
|
||||
|
||||
this.disposeFnOfItemId[itemId]?.()
|
||||
delete this.disposeFnOfItemId[itemId]
|
||||
@@ -175,8 +170,6 @@ export class ConsistentItemService extends Disposable {
|
||||
|
||||
// clear
|
||||
this.consistentItemIdsOfURI[uri.fsPath]?.delete(consistentItemId)
|
||||
if (this.consistentItemIdsOfURI[uri.fsPath]?.size === 0)
|
||||
delete this.consistentItemIdsOfURI[uri.fsPath]
|
||||
|
||||
delete this.infoOfConsistentItemId[consistentItemId]
|
||||
|
||||
|
||||
@@ -76,47 +76,55 @@ class SurroundingsRemover {
|
||||
|
||||
|
||||
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 foundCodeBlockEnd = pm.removeSuffix('```')
|
||||
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')
|
||||
pm.removeSuffix('\n') // remove the newline before ```
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
actualRecentlyAdded = (recentlyAddedTextLen: number) => {
|
||||
deltaInfo = (recentlyAddedTextLen: number) => {
|
||||
// aaaaaatextaaaaaa{recentlyAdded}
|
||||
// i ^ j
|
||||
// ^ i j len
|
||||
// |
|
||||
// recentyAddedIdx
|
||||
const recentlyAddedIdx = this.j - recentlyAddedTextLen + 1
|
||||
return this.originalS.substring(Math.max(this.i, recentlyAddedIdx), this.j + 1)
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const extractCodeFromRegular = ({ text, recentlyAddedTextLen }: { text: string, recentlyAddedTextLen: number }): [string, string] => {
|
||||
// Match either:
|
||||
// 1. ```language\n<code>```
|
||||
// 2. ```<code>```
|
||||
export const extractCodeFromRegular = ({ text, recentlyAddedTextLen }: { text: string, recentlyAddedTextLen: number }): [string, string, string] => {
|
||||
|
||||
const pm = new SurroundingsRemover(text)
|
||||
|
||||
pm.removeCodeBlock()
|
||||
|
||||
const s = pm.value()
|
||||
const actual = pm.actualRecentlyAdded(recentlyAddedTextLen)
|
||||
const [delta, ignoredSuffix] = pm.deltaInfo(recentlyAddedTextLen)
|
||||
|
||||
return [s, actual]
|
||||
return [s, delta, ignoredSuffix]
|
||||
}
|
||||
|
||||
|
||||
@@ -124,7 +132,7 @@ export const extractCodeFromRegular = ({ text, recentlyAddedTextLen }: { text: s
|
||||
|
||||
|
||||
// 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] => {
|
||||
export const extractCodeFromFIM = ({ text, recentlyAddedTextLen, midTag, }: { text: string, recentlyAddedTextLen: number, midTag: string }): [string, string, string] => {
|
||||
|
||||
/* ------------- summary of the regex -------------
|
||||
[optional ` | `` | ```]
|
||||
@@ -146,9 +154,9 @@ export const extractCodeFromFIM = ({ text, recentlyAddedTextLen, midTag, }: { te
|
||||
pm.removeSuffix(`</${midTag}>`)
|
||||
}
|
||||
const s = pm.value()
|
||||
const actual = pm.actualRecentlyAdded(recentlyAddedTextLen)
|
||||
const [delta, ignoredSuffix] = pm.deltaInfo(recentlyAddedTextLen)
|
||||
|
||||
return [s, actual]
|
||||
return [s, delta, ignoredSuffix]
|
||||
|
||||
|
||||
// // const regex = /[\s\S]*?(?:`{1,3}\s*([a-zA-Z_]+[\w]*)?[\s\S]*?)?<MID>([\s\S]*?)(?:<\/MID>|`{1,3}|$)/;
|
||||
|
||||
@@ -1,10 +1,44 @@
|
||||
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
|
||||
export const VSReadFile = async (modelService: IModelService, uri: URI): Promise<string | null> => {
|
||||
const model = modelService.getModel(uri)
|
||||
if (!model) return null
|
||||
return model.getValue(EndOfLinePreference.LF)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
@@ -25,15 +25,12 @@ import * as dom from '../../../../base/browser/dom.js';
|
||||
import { Widget } from '../../../../base/browser/ui/widget.js';
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
import { IConsistentEditorItemService, IConsistentItemService } from './helperServices/consistentItemService.js';
|
||||
import { ctrlKStream_prefixAndSuffix, ctrlKStream_prompt, ctrlKStream_systemMessage, ctrlLStream_prompt, ctrlLStream_systemMessage, defaultFimTags } from './prompt/prompts.js';
|
||||
import { ILLMMessageService } from '../../../../platform/void/common/llmMessageService.js';
|
||||
import { voidPrefixAndSuffix, ctrlKStream_userMessage, ctrlKStream_systemMessage, fastApply_rewritewholething_userMessage, fastApply_rewritewholething_systemMessage, defaultFimTags } from './prompt/prompts.js';
|
||||
|
||||
import { mountCtrlK } from '../browser/react/out/quick-edit-tsx/index.js'
|
||||
import { QuickEditPropsType } from './quickEditActions.js';
|
||||
import { errorDetails, LLMMessage } from '../../../../platform/void/common/llmMessageTypes.js';
|
||||
import { IModelContentChangedEvent } from '../../../../editor/common/textModelEvents.js';
|
||||
import { extractCodeFromFIM, extractCodeFromRegular } from './helpers/extractCodeFromResult.js';
|
||||
import { IMetricsService } from '../../../../platform/void/common/metricsService.js';
|
||||
import { filenameToVscodeLanguage } from './helpers/detectLanguage.js';
|
||||
import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js';
|
||||
import { isMacintosh } from '../../../../base/common/platform.js';
|
||||
@@ -41,6 +38,9 @@ import { EditorOption } from '../../../../editor/common/config/editorOptions.js'
|
||||
import { Emitter } from '../../../../base/common/event.js';
|
||||
import { VOID_OPEN_SETTINGS_ACTION_ID } from './voidSettingsPane.js';
|
||||
import { ICommandService } from '../../../../platform/commands/common/commands.js';
|
||||
import { ILLMMessageService } from '../common/llmMessageService.js';
|
||||
import { LLMChatMessage, errorDetails } from '../common/llmMessageTypes.js';
|
||||
import { IMetricsService } from '../common/metricsService.js';
|
||||
|
||||
const configOfBG = (color: Color) => {
|
||||
return { dark: color, light: color, hcDark: color, hcLight: color, }
|
||||
@@ -102,14 +102,14 @@ const getLeadingWhitespacePx = (editor: ICodeEditor, startLine: number): number
|
||||
|
||||
// similar to ServiceLLM
|
||||
export type StartApplyingOpts = {
|
||||
featureName: 'Ctrl+K';
|
||||
from: 'QuickEdit';
|
||||
diffareaid: number; // id of the CtrlK area (contains text selection)
|
||||
userMessage: string; // user message
|
||||
} | {
|
||||
featureName: 'Ctrl+L';
|
||||
userMessage: string;
|
||||
from: 'Chat';
|
||||
applyStr: string;
|
||||
applyBoxId: string;
|
||||
} | {
|
||||
featureName: 'Autocomplete';
|
||||
from: 'Autocomplete';
|
||||
range: IRange;
|
||||
userMessage: string;
|
||||
}
|
||||
@@ -260,6 +260,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
|
||||
if (!(model.uri.fsPath in this.diffAreasOfURI)) {
|
||||
this.diffAreasOfURI[model.uri.fsPath] = new Set();
|
||||
}
|
||||
else return // do not add listeners to the same model twice - important, or will see duplicates
|
||||
|
||||
// when the user types, realign diff areas and re-render them
|
||||
this._register(
|
||||
@@ -280,7 +281,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
|
||||
.filter(diffArea => !!diffArea && diffArea.type === 'DiffZone')
|
||||
const isStreaming = diffZones.find(diffZone => !!diffZone._streamState.isStreaming)
|
||||
if (diffZones.length !== 0 && !isStreaming && !removeAcceptRejectAllUI) {
|
||||
removeAcceptRejectAllUI = this._addAcceptRejectUI(uri) ?? null
|
||||
removeAcceptRejectAllUI = this._addAcceptRejectAllUI(uri) ?? null
|
||||
} else {
|
||||
removeAcceptRejectAllUI?.()
|
||||
removeAcceptRejectAllUI = null
|
||||
@@ -394,7 +395,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
|
||||
}
|
||||
}
|
||||
|
||||
private _addAcceptRejectUI(uri: URI) {
|
||||
private _addAcceptRejectAllUI(uri: URI) {
|
||||
|
||||
// find all diffzones that aren't streaming
|
||||
const diffZones: DiffZone[] = []
|
||||
@@ -1206,17 +1207,226 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
|
||||
return null
|
||||
}
|
||||
|
||||
// private _generateSearchAndReplaceBlocks({ filename, applyStr }: { filename: URI, applyStr: string }): DiffZone | undefined {
|
||||
|
||||
// // call LLM to generate search and replace blocks (outputs something like [{search: 'this is my code', replace: 'this is m'}, ... ])
|
||||
|
||||
// // 1a output search block
|
||||
|
||||
// let uri: URI
|
||||
|
||||
// const uri_ = this._getActiveEditorURI()
|
||||
// if (!uri_) return
|
||||
// uri = uri_
|
||||
|
||||
// // reject all diffZones on this URI, adding to history (there can't possibly be overlap after this)
|
||||
// this.removeDiffAreas({ uri, behavior: 'reject', removeCtrlKs: true })
|
||||
|
||||
// // in ctrl+L the start and end lines are the full document
|
||||
|
||||
// const numLines = this._getNumLines(uri)
|
||||
// if (numLines === null) return
|
||||
|
||||
// let startLine: number
|
||||
// let endLine: number
|
||||
|
||||
// startLine = 1
|
||||
// endLine = numLines
|
||||
|
||||
// const currentFileStr = this._readURI(uri)
|
||||
// if (currentFileStr === null) return
|
||||
// const originalCode = currentFileStr.split('\n').slice((startLine - 1), (endLine - 1) + 1).join('\n')
|
||||
|
||||
|
||||
|
||||
// // 1b find the start and end line that the search block lives on (if can't find it, retry 1a)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// let streamRequestIdRef: { current: string | null } = { current: null }
|
||||
|
||||
|
||||
// // add to history
|
||||
// const { onFinishEdit } = this._addToHistory(uri)
|
||||
|
||||
// // __TODO__ let users customize modelFimTags
|
||||
// const isOllamaFIM = false // this._voidSettingsService.state.modelSelectionOfFeature['Ctrl+K']?.providerName === 'ollama'
|
||||
// const modelFimTags = defaultFimTags
|
||||
|
||||
// const adding: Omit<DiffZone, 'diffareaid'> = {
|
||||
// type: 'DiffZone',
|
||||
// originalCode,
|
||||
// startLine,
|
||||
// endLine,
|
||||
// _URI: uri,
|
||||
// _streamState: {
|
||||
// isStreaming: true,
|
||||
// streamRequestIdRef,
|
||||
// line: startLine,
|
||||
// },
|
||||
// _diffOfId: {}, // added later
|
||||
// _removeStylesFns: new Set(),
|
||||
// }
|
||||
// const diffZone = this._addDiffArea(adding)
|
||||
// this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid })
|
||||
// this._onDidAddOrDeleteDiffZones.fire({ uri })
|
||||
|
||||
// if (from === 'QuickEdit') {
|
||||
// const { diffareaid } = opts
|
||||
// const ctrlKZone = this.diffAreaOfId[diffareaid]
|
||||
// if (ctrlKZone.type !== 'CtrlKZone') return
|
||||
|
||||
// ctrlKZone._linkedStreamingDiffZone = diffZone.diffareaid
|
||||
// }
|
||||
|
||||
// // now handle messages
|
||||
// let messages: LLMChatMessage[]
|
||||
|
||||
// if (from === 'Chat') {
|
||||
// const userContent = fastApply_searchreplace_userMessage({ originalCode, applyStr: opts.applyStr, uri })
|
||||
// messages = [
|
||||
// { role: 'system', content: fastApply_rewritewholething_systemMessage, },
|
||||
// { role: 'user', content: userContent, }
|
||||
// ]
|
||||
// }
|
||||
// else if (from === 'QuickEdit') {
|
||||
// const { diffareaid } = opts
|
||||
// const ctrlKZone = this.diffAreaOfId[diffareaid]
|
||||
// if (ctrlKZone.type !== 'CtrlKZone') return
|
||||
// const { _mountInfo } = ctrlKZone
|
||||
// const instructions = _mountInfo?.textAreaRef.current?.value ?? ''
|
||||
|
||||
// // __TODO__ use Ollama's FIM api, if (isOllamaFIM) {...} else:
|
||||
// const { prefix, suffix } = voidPrefixAndSuffix({ fullFileStr: currentFileStr, startLine, endLine })
|
||||
// // if (isOllamaFIM) {
|
||||
// // messages = {
|
||||
// // type: 'ollamaFIM',
|
||||
// // prefix,
|
||||
// // suffix,
|
||||
// // }
|
||||
|
||||
// // }
|
||||
// // else {
|
||||
// const language = filenameToVscodeLanguage(uri.fsPath) ?? ''
|
||||
// const userContent = ctrlKStream_userMessage({ selection: originalCode, instructions: instructions, prefix, suffix, isOllamaFIM: false, fimTags: modelFimTags, language })
|
||||
// // type: 'messages',
|
||||
// messages = [
|
||||
// { role: 'system', content: ctrlKStream_systemMessage({ fimTags: modelFimTags }), },
|
||||
// { role: 'user', content: userContent, }
|
||||
// ]
|
||||
// // }
|
||||
// }
|
||||
// else { throw new Error(`featureName ${from} is invalid`) }
|
||||
|
||||
|
||||
// const onDone = (hadError: boolean) => {
|
||||
// diffZone._streamState = { isStreaming: false, }
|
||||
// this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid })
|
||||
|
||||
// if (from === 'QuickEdit') {
|
||||
// const ctrlKZone = this.diffAreaOfId[opts.diffareaid] as CtrlKZone
|
||||
|
||||
// ctrlKZone._linkedStreamingDiffZone = null
|
||||
// this._deleteCtrlKZone(ctrlKZone)
|
||||
// }
|
||||
// this._refreshStylesAndDiffsInURI(uri)
|
||||
// onFinishEdit()
|
||||
|
||||
// // if had error, revert!
|
||||
// if (hadError) {
|
||||
// this._undoHistory(diffZone._URI)
|
||||
// }
|
||||
// }
|
||||
|
||||
// // refresh now in case onText takes a while to get 1st message
|
||||
// this._refreshStylesAndDiffsInURI(uri)
|
||||
|
||||
|
||||
// const extractText = (fullText: string, recentlyAddedTextLen: number) => {
|
||||
// if (from === 'QuickEdit') {
|
||||
// if (isOllamaFIM) return fullText
|
||||
// return extractCodeFromFIM({ text: fullText, recentlyAddedTextLen, midTag: modelFimTags.midTag })
|
||||
// }
|
||||
// else if (from === 'Chat') {
|
||||
// return extractCodeFromRegular({ text: fullText, recentlyAddedTextLen })
|
||||
// }
|
||||
// throw 1
|
||||
// }
|
||||
|
||||
// const latestStreamInfo = { line: diffZone.startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 }
|
||||
|
||||
|
||||
// // state used in onText:
|
||||
// let fullText = ''
|
||||
// let prevIgnoredSuffix = ''
|
||||
|
||||
// streamRequestIdRef.current = this._llmMessageService.sendLLMMessage({
|
||||
// messagesType: 'chatMessages',
|
||||
// useProviderFor: opts.from === 'Chat' ? 'FastApply' : 'Ctrl+K',
|
||||
// logging: { loggingName: `startApplying - ${from}` },
|
||||
// messages,
|
||||
// onText: ({ newText: newText_ }) => {
|
||||
|
||||
// const newText = prevIgnoredSuffix + newText_ // add the previously ignored suffix because it's no longer the suffix!
|
||||
// fullText += prevIgnoredSuffix + newText
|
||||
|
||||
// const [text, deltaText, ignoredSuffix] = extractText(fullText, newText.length)
|
||||
// this._writeStreamedDiffZoneLLMText(diffZone, text, deltaText, latestStreamInfo)
|
||||
// this._refreshStylesAndDiffsInURI(uri)
|
||||
|
||||
// prevIgnoredSuffix = ignoredSuffix
|
||||
// },
|
||||
// onFinalMessage: ({ fullText }) => {
|
||||
// // console.log('DONE! FULL TEXT\n', extractText(fullText), diffZone.startLine, diffZone.endLine)
|
||||
// // at the end, re-write whole thing to make sure no sync errors
|
||||
// const [text, _] = extractText(fullText, 0)
|
||||
// this._writeText(uri, text,
|
||||
// { startLineNumber: diffZone.startLine, startColumn: 1, endLineNumber: diffZone.endLine, endColumn: Number.MAX_SAFE_INTEGER }, // 1-indexed
|
||||
// { shouldRealignDiffAreas: true }
|
||||
// )
|
||||
// onDone(false)
|
||||
// },
|
||||
// onError: (e) => {
|
||||
// const details = errorDetails(e.fullError)
|
||||
// this._notificationService.notify({
|
||||
// severity: Severity.Warning,
|
||||
// message: `Void Error: ${e.message}`,
|
||||
// actions: {
|
||||
// secondary: [{
|
||||
// id: 'void.onerror.opensettings',
|
||||
// enabled: true,
|
||||
// label: 'Open Void settings',
|
||||
// tooltip: '',
|
||||
// class: undefined,
|
||||
// run: () => { this._commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID) }
|
||||
// }]
|
||||
// },
|
||||
// source: details ? `(Hold ${isMacintosh ? 'Option' : 'Alt'} to hover) - ${details}` : undefined
|
||||
// })
|
||||
// onDone(true)
|
||||
// },
|
||||
|
||||
// })
|
||||
|
||||
// return diffZone
|
||||
|
||||
// }
|
||||
|
||||
|
||||
|
||||
private _initializeStartApplying(opts: StartApplyingOpts): DiffZone | undefined {
|
||||
|
||||
const { featureName } = opts
|
||||
const { from } = opts
|
||||
|
||||
let startLine: number
|
||||
let endLine: number
|
||||
let uri: URI
|
||||
let userMessage: string
|
||||
|
||||
if (featureName === 'Ctrl+L') {
|
||||
if (from === 'Chat') {
|
||||
|
||||
const uri_ = this._getActiveEditorURI()
|
||||
if (!uri_) return
|
||||
@@ -1231,23 +1441,19 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
|
||||
startLine = 1
|
||||
endLine = numLines
|
||||
|
||||
userMessage = opts.userMessage
|
||||
}
|
||||
else if (featureName === 'Ctrl+K') {
|
||||
else if (from === 'QuickEdit') {
|
||||
const { diffareaid } = opts
|
||||
const ctrlKZone = this.diffAreaOfId[diffareaid]
|
||||
if (ctrlKZone.type !== 'CtrlKZone') return
|
||||
|
||||
const { startLine: startLine_, endLine: endLine_, _URI, _mountInfo } = ctrlKZone
|
||||
const { startLine: startLine_, endLine: endLine_, _URI } = ctrlKZone
|
||||
uri = _URI
|
||||
startLine = startLine_
|
||||
endLine = endLine_
|
||||
|
||||
if (!_mountInfo?.textAreaRef.current) return
|
||||
userMessage = _mountInfo.textAreaRef.current?.value
|
||||
}
|
||||
else {
|
||||
throw new Error(`Void: diff.type not recognized on: ${featureName}`)
|
||||
throw new Error(`Void: diff.type not recognized on: ${from}`)
|
||||
}
|
||||
|
||||
const currentFileStr = this._readURI(uri)
|
||||
@@ -1283,7 +1489,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
|
||||
this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid })
|
||||
this._onDidAddOrDeleteDiffZones.fire({ uri })
|
||||
|
||||
if (featureName === 'Ctrl+K') {
|
||||
if (from === 'QuickEdit') {
|
||||
const { diffareaid } = opts
|
||||
const ctrlKZone = this.diffAreaOfId[diffareaid]
|
||||
if (ctrlKZone.type !== 'CtrlKZone') return
|
||||
@@ -1292,38 +1498,50 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
|
||||
}
|
||||
|
||||
// now handle messages
|
||||
let messages: LLMMessage[]
|
||||
let messages: LLMChatMessage[]
|
||||
|
||||
if (featureName === 'Ctrl+L') {
|
||||
const userContent = ctrlLStream_prompt({ originalCode, userMessage, uri })
|
||||
if (from === 'Chat') {
|
||||
const userContent = fastApply_rewritewholething_userMessage({ originalCode, applyStr: opts.applyStr, uri })
|
||||
messages = [
|
||||
{ role: 'system', content: ctrlLStream_systemMessage, },
|
||||
{ role: 'system', content: fastApply_rewritewholething_systemMessage, },
|
||||
{ role: 'user', content: userContent, }
|
||||
]
|
||||
}
|
||||
else if (featureName === 'Ctrl+K') {
|
||||
const { prefix, suffix } = ctrlKStream_prefixAndSuffix({ fullFileStr: currentFileStr, startLine, endLine })
|
||||
// console.log('PREFIX:\n', prefix)
|
||||
// console.log('SUFFIX:\n', suffix)
|
||||
// console.log('USER CONTENT:\n', userContent)
|
||||
else if (from === 'QuickEdit') {
|
||||
const { diffareaid } = opts
|
||||
const ctrlKZone = this.diffAreaOfId[diffareaid]
|
||||
if (ctrlKZone.type !== 'CtrlKZone') return
|
||||
const { _mountInfo } = ctrlKZone
|
||||
const instructions = _mountInfo?.textAreaRef.current?.value ?? ''
|
||||
|
||||
// __TODO__ use Ollama's FIM api
|
||||
// if (isOllamaFIM) {...} else:
|
||||
// __TODO__ use Ollama's FIM api, if (isOllamaFIM) {...} else:
|
||||
const { prefix, suffix } = voidPrefixAndSuffix({ fullFileStr: currentFileStr, startLine, endLine })
|
||||
// if (isOllamaFIM) {
|
||||
// messages = {
|
||||
// type: 'ollamaFIM',
|
||||
// prefix,
|
||||
// suffix,
|
||||
// }
|
||||
|
||||
// }
|
||||
// else {
|
||||
const language = filenameToVscodeLanguage(uri.fsPath) ?? ''
|
||||
const userContent = ctrlKStream_prompt({ selection: originalCode, userMessage, prefix, suffix, isOllamaFIM: false, fimTags: modelFimTags, language })
|
||||
const userContent = ctrlKStream_userMessage({ selection: originalCode, instructions: instructions, prefix, suffix, isOllamaFIM: false, fimTags: modelFimTags, language })
|
||||
// type: 'messages',
|
||||
messages = [
|
||||
{ role: 'system', content: ctrlKStream_systemMessage, },
|
||||
{ role: 'system', content: ctrlKStream_systemMessage({ fimTags: modelFimTags }), },
|
||||
{ role: 'user', content: userContent, }
|
||||
]
|
||||
// }
|
||||
}
|
||||
else { throw new Error(`featureName ${featureName} is invalid`) }
|
||||
else { throw new Error(`featureName ${from} is invalid`) }
|
||||
|
||||
|
||||
const onDone = (hadError: boolean) => {
|
||||
diffZone._streamState = { isStreaming: false, }
|
||||
this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid })
|
||||
|
||||
if (featureName === 'Ctrl+K') {
|
||||
if (from === 'QuickEdit') {
|
||||
const ctrlKZone = this.diffAreaOfId[opts.diffareaid] as CtrlKZone
|
||||
|
||||
ctrlKZone._linkedStreamingDiffZone = null
|
||||
@@ -1343,26 +1561,37 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
|
||||
|
||||
|
||||
const extractText = (fullText: string, recentlyAddedTextLen: number) => {
|
||||
if (featureName === 'Ctrl+K') {
|
||||
if (from === 'QuickEdit') {
|
||||
if (isOllamaFIM) return fullText
|
||||
return extractCodeFromFIM({ text: fullText, recentlyAddedTextLen, midTag: modelFimTags.midTag })
|
||||
}
|
||||
else if (featureName === 'Ctrl+L') {
|
||||
else if (from === 'Chat') {
|
||||
return extractCodeFromRegular({ text: fullText, recentlyAddedTextLen })
|
||||
}
|
||||
throw 1
|
||||
}
|
||||
|
||||
const latestStreamInfo = { line: diffZone.startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 }
|
||||
streamRequestIdRef.current = this._llmMessageService.sendLLMMessage({
|
||||
useProviderFor: featureName,
|
||||
logging: { loggingName: `startApplying - ${featureName}` },
|
||||
messages,
|
||||
onText: ({ newText, fullText }) => {
|
||||
const [text, deltaText] = extractText(fullText, newText.length)
|
||||
|
||||
// state used in onText:
|
||||
let fullText = ''
|
||||
let prevIgnoredSuffix = ''
|
||||
|
||||
streamRequestIdRef.current = this._llmMessageService.sendLLMMessage({
|
||||
messagesType: 'chatMessages',
|
||||
useProviderFor: opts.from === 'Chat' ? 'FastApply' : 'Ctrl+K',
|
||||
logging: { loggingName: `startApplying - ${from}` },
|
||||
messages,
|
||||
onText: ({ newText: newText_ }) => {
|
||||
|
||||
const newText = prevIgnoredSuffix + newText_ // add the previously ignored suffix because it's no longer the suffix!
|
||||
fullText += prevIgnoredSuffix + newText
|
||||
|
||||
const [text, deltaText, ignoredSuffix] = extractText(fullText, newText.length)
|
||||
this._writeStreamedDiffZoneLLMText(diffZone, text, deltaText, latestStreamInfo)
|
||||
this._refreshStylesAndDiffsInURI(uri)
|
||||
|
||||
prevIgnoredSuffix = ignoredSuffix
|
||||
},
|
||||
onFinalMessage: ({ fullText }) => {
|
||||
// console.log('DONE! FULL TEXT\n', extractText(fullText), diffZone.startLine, diffZone.endLine)
|
||||
@@ -1394,7 +1623,6 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
|
||||
onDone(true)
|
||||
},
|
||||
|
||||
range: { startLineNumber: startLine, endLineNumber: endLine, startColumn: 1, endColumn: Number.MAX_SAFE_INTEGER },
|
||||
})
|
||||
|
||||
return diffZone
|
||||
|
||||
@@ -6,54 +6,96 @@
|
||||
|
||||
import { URI } from '../../../../../base/common/uri.js';
|
||||
import { filenameToVscodeLanguage } from '../helpers/detectLanguage.js';
|
||||
import { CodeSelection } from '../chatThreadService.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 = ['```', '```']
|
||||
|
||||
export const chat_systemMessage = `\
|
||||
You are a coding assistant. You are given a list of relevant files \`files\`, a selection that the user is making \`selection\`, and instructions to follow \`instructions\`.
|
||||
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\`.
|
||||
|
||||
Please edit the selected file following the user's instructions (or, if appropriate, answer their question instead).
|
||||
Please respond to the user's query.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
## EXAMPLE
|
||||
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.
|
||||
|
||||
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
|
||||
selected file \`math.ts\`:
|
||||
\`\`\` typescript
|
||||
math.ts
|
||||
${tripleTick[0]}typescript
|
||||
const addNumbers = (a, b) => a + b
|
||||
const multiplyNumbers = (a, b) => a * b
|
||||
const subtractNumbers = (a, b) => a - b
|
||||
const divideNumbers = (a, b) => a / b
|
||||
\`\`\`
|
||||
|
||||
SELECTION
|
||||
\`\`\` typescript
|
||||
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
|
||||
const subtractNumbers = (a, b) => a - b
|
||||
\`\`\`
|
||||
${tripleTick[1]}
|
||||
|
||||
INSTRUCTIONS
|
||||
\`\`\` typescript
|
||||
add a function that multiplies numbers below this
|
||||
\`\`\`
|
||||
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
|
||||
|
||||
EXPECTED OUTPUT
|
||||
ACCEPTED OUTPUT
|
||||
We can add the following code to the file:
|
||||
\`\`\` typescript
|
||||
${tripleTick[0]}typescript
|
||||
// existing code...
|
||||
const subtractNumbers = (a, b) => a - b;
|
||||
const multiplyNumbers = (a, b) => a * b;
|
||||
const subtractNumbers = (a, b) => a - b
|
||||
const exponentiateNumbers = (a, b) => Math.pow(a, b)
|
||||
const divideNumbers = (a, b) => a / b
|
||||
// existing code...
|
||||
\`\`\`
|
||||
|
||||
## EXAMPLE
|
||||
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 2
|
||||
FILES
|
||||
selected file \`fib.ts\`:
|
||||
\`\`\` typescript
|
||||
fib.ts
|
||||
${tripleTick[0]}typescript
|
||||
|
||||
const dfs = (root) => {
|
||||
if (!root) return;
|
||||
@@ -65,21 +107,20 @@ const fib = (n) => {
|
||||
if (n < 1) return 1
|
||||
return fib(n - 1) + fib(n - 2)
|
||||
}
|
||||
\`\`\`
|
||||
${tripleTick[1]}
|
||||
|
||||
SELECTION
|
||||
\`\`\` typescript
|
||||
SELECTIONS
|
||||
fib.ts (lines 10:10)
|
||||
${tripleTick[0]}typescript
|
||||
return fib(n - 1) + fib(n - 2)
|
||||
\`\`\`
|
||||
${tripleTick[1]}
|
||||
|
||||
INSTRUCTIONS
|
||||
\`\`\` typescript
|
||||
memoize results
|
||||
\`\`\`
|
||||
|
||||
EXPECTED OUTPUT
|
||||
ACCEPTED OUTPUT
|
||||
To implement memoization in your Fibonacci function, you can use a JavaScript object to store previously computed results. This will help avoid redundant calculations and improve performance. Here's how you can modify your function:
|
||||
\`\`\` typescript
|
||||
${tripleTick[0]}typescript
|
||||
// existing code...
|
||||
const fib = (n, memo = {}) => {
|
||||
if (n < 1) return 1;
|
||||
@@ -87,7 +128,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.
|
||||
@@ -97,191 +138,167 @@ Store Result: After computing fib(n), the result is stored in memo for future re
|
||||
`
|
||||
|
||||
|
||||
|
||||
const stringifySelections = (selections: CodeSelection[]) => {
|
||||
return selections.map(({ fileURI, content, selectionStr }) =>
|
||||
`\
|
||||
File: ${fileURI.fsPath}
|
||||
\`\`\` ${filenameToVscodeLanguage(fileURI.fsPath) ?? ''}
|
||||
${content // this was the enite file which is foolish
|
||||
}
|
||||
\`\`\`${selectionStr === null ? '' : `
|
||||
Selection: ${selectionStr}`}
|
||||
`).join('\n')
|
||||
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_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`
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
str += `${instructions}`;
|
||||
|
||||
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`
|
||||
|
||||
return str;
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
export const ctrlLStream_systemMessage = `
|
||||
You are a coding assistant that applies a diff to a file. You are given the original file \`original_file\`, a diff \`diff\`, and a new file that you are applying the diff to \`new_file\`.
|
||||
|
||||
Please finish writing the new file \`new_file\`, according to the diff \`diff\`. You must completely re-write the whole file, using the diff.
|
||||
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\`.
|
||||
|
||||
Directions:
|
||||
1. Continue exactly where the new file \`new_file\` left off.
|
||||
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. Note that \`+\` lines represent additions, \`-\` lines represent removals, and space lines \` \` represent no change.
|
||||
|
||||
# Example 1:
|
||||
|
||||
ORIGINAL_FILE
|
||||
\`Sidebar.tsx\`:
|
||||
\`\`\` typescript
|
||||
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
|
||||
\`\`\` typescript
|
||||
@@ ... @@
|
||||
-<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
|
||||
\`\`\` typescript
|
||||
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
|
||||
\`\`\` typescript
|
||||
<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;\`\`\`
|
||||
3. ONLY output the full new file. Do not add any other explanations or text.
|
||||
`
|
||||
|
||||
|
||||
|
||||
|
||||
export const ctrlLStream_prompt = ({ originalCode, userMessage, uri }: { originalCode: string, userMessage: string, uri: URI }) => {
|
||||
export const fastApply_rewritewholething_userMessage = ({ originalCode, applyStr, uri }: { originalCode: string, applyStr: string, uri: URI }) => {
|
||||
|
||||
const language = filenameToVscodeLanguage(uri.fsPath) ?? ''
|
||||
|
||||
return `\
|
||||
ORIGINAL_CODE
|
||||
\`\`\` ${language}
|
||||
ORIGINAL_FILE
|
||||
${tripleTick[0]}${language}
|
||||
${originalCode}
|
||||
\`\`\`
|
||||
${tripleTick[1]}
|
||||
|
||||
DIFF
|
||||
\`\`\`
|
||||
${userMessage}
|
||||
\`\`\`
|
||||
CHANGE
|
||||
${tripleTick[0]}
|
||||
${applyStr}
|
||||
${tripleTick[1]}
|
||||
|
||||
INSTRUCTIONS
|
||||
Please finish writing the new file by applying the diff to the original file. Return ONLY the completion of the file, without any explanation.
|
||||
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 ctrlKStream_systemMessage = `\
|
||||
|
||||
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_prefixAndSuffix = ({ fullFileStr, startLine, endLine }: { fullFileStr: string, startLine: number, endLine: number }) => {
|
||||
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 }) => {
|
||||
|
||||
const fullFileLines = fullFileStr.split('\n')
|
||||
|
||||
@@ -336,16 +353,31 @@ export type FimTagsType = {
|
||||
midTag: string
|
||||
}
|
||||
export const defaultFimTags: FimTagsType = {
|
||||
preTag: 'BEFORE',
|
||||
sufTag: 'AFTER',
|
||||
preTag: 'ABOVE',
|
||||
sufTag: 'BELOW',
|
||||
midTag: 'SELECTION',
|
||||
}
|
||||
|
||||
export const ctrlKStream_prompt = ({ selection, prefix, suffix, userMessage, fimTags, isOllamaFIM, language }:
|
||||
{
|
||||
selection: string, prefix: string, suffix: string, userMessage: string, fimTags: FimTagsType, language: string,
|
||||
isOllamaFIM: false, // we require this be false for clarity
|
||||
}) => {
|
||||
// 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
|
||||
@@ -353,300 +385,20 @@ export const ctrlKStream_prompt = ({ selection, prefix, suffix, userMessage, fim
|
||||
// const sufTag = 'AFTER'
|
||||
// const midTag = 'SELECTION'
|
||||
return `\
|
||||
The user is selecting this code as their SELECTION:
|
||||
\`\`\` ${language}
|
||||
|
||||
CURRENT SELECTION
|
||||
${tripleTick[0]}${language}
|
||||
<${midTag}>${selection}</${midTag}>
|
||||
\`\`\`
|
||||
${tripleTick[1]}
|
||||
|
||||
The user wants to apply the following INSTRUCTIONS to the SELECTION:
|
||||
${userMessage}
|
||||
INSTRUCTIONS
|
||||
${instructions}
|
||||
|
||||
Please edit the SELECTION following the user's INSTRUCTIONS, and return the edited selection.
|
||||
|
||||
Note that the SELECTION has code that comes before it. This code is indicated with <${preTag}>...before</${preTag}>.
|
||||
Note also that the SELECTION has code that comes after it. This code is indicated with <${sufTag}>...after</${sufTag}>.
|
||||
|
||||
Instructions:
|
||||
1. Your OUTPUT should be a SINGLE PIECE OF CODE of the form <${midTag}>...new_selection</${midTag}>. Do NOT output any text or explanations before or after this.
|
||||
2. You may ONLY CHANGE the original SELECTION, and NOT the content in the <${preTag}>...</${preTag}> or <${sufTag}>...</${sufTag}> tags.
|
||||
3. Make sure all brackets in the new selection are balanced the same as in the original selection.
|
||||
4. Be careful not to duplicate or remove variables, comments, or other syntax by mistake.
|
||||
|
||||
Given the code:
|
||||
<${preTag}>${prefix}</${preTag}>
|
||||
<${sufTag}>${suffix}</${sufTag}>
|
||||
|
||||
Return only the completion block of code (of the form \`\`\` ${language}\n <${midTag}>...new_selection</${midTag}>\`\`\`):`
|
||||
Return only the completion block of code (of the form ${tripleTick[0]}${language}
|
||||
<${midTag}>...new code</${midTag}>
|
||||
${tripleTick[1]}).`
|
||||
};
|
||||
|
||||
|
||||
|
||||
// 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>
|
||||
// \`\`\`
|
||||
// `;
|
||||
|
||||
@@ -7,12 +7,12 @@ 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';
|
||||
|
||||
|
||||
export type QuickEditPropsType = {
|
||||
|
||||
@@ -80,6 +80,22 @@ 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',
|
||||
|
||||
@@ -13,10 +13,9 @@ export const BlockCode = ({ buttonsOnHover, ...codeEditorProps }: { buttonsOnHov
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative group w-full overflow-hidden">
|
||||
<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={`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>
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
import React, { JSX, useCallback, useEffect, useState } from 'react'
|
||||
import { marked, MarkedToken, Token } from 'marked'
|
||||
import { BlockCode } from './BlockCode.js'
|
||||
import { useAccessor } from '../util/services.js'
|
||||
import { useAccessor, useChatThreadsState, useChatThreadsStreamState } from '../util/services.js'
|
||||
import { ChatLocation, getApplyBoxId, } from '../../../searchAndReplaceService.js'
|
||||
import { nameToVscodeLanguage } from '../../../helpers/detectLanguage.js'
|
||||
|
||||
|
||||
@@ -18,7 +19,7 @@ enum CopyButtonState {
|
||||
|
||||
const COPY_FEEDBACK_TIMEOUT = 1000 // amount of time to say 'Copied!'
|
||||
|
||||
const CodeButtonsOnHover = ({ text }: { text: string }) => {
|
||||
const ApplyButtonsOnHover = ({ applyStr, applyBoxId }: { applyStr: string, applyBoxId: string }) => {
|
||||
const accessor = useAccessor()
|
||||
|
||||
const [copyButtonState, setCopyButtonState] = useState(CopyButtonState.Copy)
|
||||
@@ -36,22 +37,24 @@ const CodeButtonsOnHover = ({ text }: { text: string }) => {
|
||||
}, [copyButtonState])
|
||||
|
||||
const onCopy = useCallback(() => {
|
||||
clipboardService.writeText(text)
|
||||
clipboardService.writeText(applyStr)
|
||||
.then(() => { setCopyButtonState(CopyButtonState.Copied) })
|
||||
.catch(() => { setCopyButtonState(CopyButtonState.Error) })
|
||||
metricsService.capture('Copy Code', { length: text.length }) // capture the length only
|
||||
metricsService.capture('Copy Code', { length: applyStr.length }) // capture the length only
|
||||
|
||||
}, [text, clipboardService])
|
||||
}, [metricsService, clipboardService, applyStr])
|
||||
|
||||
const onApply = useCallback(() => {
|
||||
inlineDiffService.startApplying({
|
||||
featureName: 'Ctrl+L',
|
||||
userMessage: text,
|
||||
})
|
||||
metricsService.capture('Apply Code', { length: text.length }) // capture the length only
|
||||
}, [inlineDiffService])
|
||||
|
||||
const isSingleLine = !text.includes('\n')
|
||||
inlineDiffService.startApplying({
|
||||
from: 'Chat',
|
||||
applyStr,
|
||||
applyBoxId,
|
||||
})
|
||||
metricsService.capture('Apply Code', { length: applyStr.length }) // capture the length only
|
||||
}, [metricsService, inlineDiffService, applyStr])
|
||||
|
||||
const isSingleLine = !applyStr.includes('\n')
|
||||
|
||||
return <>
|
||||
<button
|
||||
@@ -84,7 +87,8 @@ export const CodeSpan = ({ children, className }: { children: React.ReactNode, c
|
||||
</code>
|
||||
}
|
||||
|
||||
const RenderToken = ({ token, nested = false, noSpace = false }: { token: Token | string, nested?: boolean, noSpace?: boolean }): JSX.Element => {
|
||||
const RenderToken = ({ token, nested = false, noSpace = false, chatLocation, tokenId = '', }: { token: Token | string, nested?: boolean, noSpace?: boolean, chatLocation?: ChatLocation, tokenId?: string, }): JSX.Element => {
|
||||
|
||||
|
||||
// deal with built-in tokens first (assume marked token)
|
||||
const t = token as MarkedToken
|
||||
@@ -94,10 +98,18 @@ const RenderToken = ({ token, nested = false, noSpace = false }: { token: Token
|
||||
}
|
||||
|
||||
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]} // use vscode to detect language
|
||||
buttonsOnHover={<CodeButtonsOnHover text={t.text} />}
|
||||
language={t.lang === undefined ? undefined : nameToVscodeLanguage[t.lang]}
|
||||
buttonsOnHover={<ApplyButtonsOnHover applyStr={t.text} applyBoxId={applyBoxId} />}
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -183,21 +195,21 @@ const RenderToken = ({ token, nested = false, noSpace = false }: { token: Token
|
||||
if (t.type === "paragraph") {
|
||||
const contents = <>
|
||||
{t.tokens.map((token, index) => (
|
||||
<RenderToken key={index} token={token} />
|
||||
<RenderToken key={index} token={token} tokenId={`${tokenId}-${index}`} /> // assign a unique tokenId to nested components
|
||||
))}
|
||||
</>
|
||||
if (nested)
|
||||
return contents
|
||||
return <p className={`${noSpace ? '' : 'my-4'} leading`}>{contents}</p>
|
||||
if (nested) return contents
|
||||
|
||||
return <p className={`${noSpace ? '' : 'my-4'}`}>
|
||||
{contents}
|
||||
</p>
|
||||
}
|
||||
|
||||
if (t.type === "html") {
|
||||
return (
|
||||
<pre className={`bg-4oid-bg-2 p-4 rounded-lg ${noSpace ? '' : 'my-4'} font-mono text-sm`}>
|
||||
{`<html>`}
|
||||
<p className={`${noSpace ? '' : 'my-4'}`}>
|
||||
{t.raw}
|
||||
{`</html>`}
|
||||
</pre>
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -266,12 +278,12 @@ const RenderToken = ({ token, nested = false, noSpace = false }: { token: Token
|
||||
)
|
||||
}
|
||||
|
||||
export const ChatMarkdownRender = ({ string, nested = false, noSpace }: { string: string, nested?: boolean, noSpace?: boolean }) => {
|
||||
export const ChatMarkdownRender = ({ string, nested = false, noSpace, chatLocation }: { string: string, nested?: boolean, noSpace?: boolean, chatLocation?: ChatLocation }) => {
|
||||
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} />
|
||||
<RenderToken key={index} token={token} nested={nested} noSpace={noSpace} chatLocation={chatLocation} />
|
||||
))}
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -7,11 +7,12 @@ import React, { FormEvent, useCallback, useEffect, useRef, useState } from 'reac
|
||||
import { useSettingsState, useSidebarState, useChatThreadsState, useQuickEditState, useAccessor } from '../util/services.js';
|
||||
import { TextAreaFns, VoidInputBox2 } from '../util/inputs.js';
|
||||
import { QuickEditPropsType } from '../../../quickEditActions.js';
|
||||
import { ButtonStop, ButtonSubmit, IconX } from '../sidebar-tsx/SidebarChat.js';
|
||||
import { 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,
|
||||
@@ -42,23 +43,23 @@ export const QuickEditChat = ({
|
||||
}, [onChangeHeight]);
|
||||
|
||||
|
||||
const settingsState = useSettingsState()
|
||||
|
||||
// state of current message
|
||||
const [instructionsAreEmpty, setInstructionsAreEmpty] = useState(!(initText ?? '')) // the user's instructions
|
||||
const isDisabled = instructionsAreEmpty
|
||||
const isDisabled = instructionsAreEmpty || !!isFeatureNameDisabled('Ctrl+K', settingsState)
|
||||
|
||||
const [currStreamingDiffZoneRef, setCurrentlyStreamingDiffZone] = useRefState<number | null>(initStreamingDiffZoneId)
|
||||
const isStreaming = currStreamingDiffZoneRef.current !== null
|
||||
|
||||
const onSubmit = useCallback((e: FormEvent) => {
|
||||
const onSubmit = useCallback(() => {
|
||||
if (isDisabled) return
|
||||
if (currStreamingDiffZoneRef.current !== null) return
|
||||
textAreaFnsRef.current?.disable()
|
||||
|
||||
const instructions = textAreaRef.current?.value ?? ''
|
||||
const id = inlineDiffsService.startApplying({
|
||||
featureName: 'Ctrl+K',
|
||||
from: 'QuickEdit',
|
||||
diffareaid: diffareaid,
|
||||
userMessage: instructions,
|
||||
})
|
||||
setCurrentlyStreamingDiffZone(id ?? null)
|
||||
}, [currStreamingDiffZoneRef, setCurrentlyStreamingDiffZone, isDisabled, inlineDiffsService, diffareaid])
|
||||
@@ -80,110 +81,45 @@ export const QuickEditChat = ({
|
||||
|
||||
const keybindingString = accessor.get('IKeybindingService').lookupKeybinding(VOID_CTRL_K_ACTION_ID)?.getLabel()
|
||||
|
||||
return <div ref={sizerRef} style={{ maxWidth: 500 }} className={`py-2 w-full`}>
|
||||
<form
|
||||
// copied from SidebarChat.tsx
|
||||
className={`
|
||||
flex flex-col gap-2 p-2 relative input text-left shrink-0
|
||||
transition-all duration-200
|
||||
rounded-md
|
||||
bg-vscode-input-bg
|
||||
border border-void-border-3 focus-within:border-void-border-1 hover:border-void-border-1
|
||||
`}
|
||||
onClick={(e) => {
|
||||
textAreaRef.current?.focus()
|
||||
}}
|
||||
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() }}
|
||||
>
|
||||
|
||||
{/* // this div is used to position the input box properly */}
|
||||
<div
|
||||
className={`w-full z-[999] relative`}
|
||||
>
|
||||
<div className='flex flex-row items-center justify-between items-end gap-1'>
|
||||
|
||||
{/* input */}
|
||||
<div // copied from SidebarChat.tsx
|
||||
className={`w-full`}
|
||||
>
|
||||
{/* text input */}
|
||||
<VoidInputBox2
|
||||
className='px-1'
|
||||
initValue={initText}
|
||||
|
||||
ref={useCallback((r: HTMLTextAreaElement | null) => {
|
||||
textAreaRef.current = r
|
||||
textAreaRef_(r)
|
||||
|
||||
// if presses the esc key, X
|
||||
r?.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape')
|
||||
onX()
|
||||
})
|
||||
|
||||
}, [textAreaRef_, onX])}
|
||||
|
||||
fnsRef={textAreaFnsRef}
|
||||
|
||||
placeholder={`Enter instructions...`}
|
||||
// ${keybindingString} to select.
|
||||
|
||||
onChangeText={useCallback((newStr: string) => {
|
||||
setInstructionsAreEmpty(!newStr)
|
||||
onChangeText_(newStr)
|
||||
}, [onChangeText_])}
|
||||
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
onSubmit(e)
|
||||
return
|
||||
}
|
||||
}}
|
||||
|
||||
multiline={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* X button */}
|
||||
<div className='absolute -top-1 -right-1 cursor-pointer z-1'>
|
||||
<IconX
|
||||
size={16}
|
||||
className="p-[1px] stroke-[2] opacity-80 text-void-fg-3 hover:brightness-95"
|
||||
onClick={onX}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* bottom row */}
|
||||
<div
|
||||
className='flex flex-row justify-between items-end gap-1'
|
||||
>
|
||||
{/* submit options */}
|
||||
<div className='max-w-[150px]
|
||||
@@[&_select]:!void-border-none
|
||||
@@[&_select]:!void-outline-none'
|
||||
>
|
||||
<ModelDropdown featureName='Ctrl+K' />
|
||||
</div>
|
||||
|
||||
{/* submit / stop button */}
|
||||
{isStreaming ?
|
||||
// stop button
|
||||
<ButtonStop
|
||||
onClick={onInterrupt}
|
||||
/>
|
||||
:
|
||||
// submit button (up arrow)
|
||||
<ButtonSubmit
|
||||
onClick={onSubmit}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
<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
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</form>
|
||||
}}
|
||||
multiline={true}
|
||||
/>
|
||||
</VoidChatArea>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
@@ -5,11 +5,12 @@
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { AlertCircle, ChevronDown, ChevronUp, X } from 'lucide-react';
|
||||
import { errorDetails } from '../../../../../../../platform/void/common/llmMessageTypes.js';
|
||||
import { errorDetails } from '../../../../../../../workbench/contrib/void/common/llmMessageTypes.js';
|
||||
import { useSettingsState } from '../util/services.js';
|
||||
|
||||
|
||||
export const ErrorDisplay = ({
|
||||
message:message_,
|
||||
message: message_,
|
||||
fullError,
|
||||
onDismiss,
|
||||
showDismiss,
|
||||
@@ -22,9 +23,9 @@ export const ErrorDisplay = ({
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
const details = errorDetails(fullError)
|
||||
const isExpandable = !!details
|
||||
|
||||
const message = message_ === 'TypeError: fetch failed' ? 'TypeError: fetch failed. This likely means you specified the wrong endpoint in Void Settings.' : message_
|
||||
|
||||
const message = message_ + ''
|
||||
|
||||
return (
|
||||
<div className={`rounded-lg border border-red-200 bg-red-50 p-4 overflow-auto`}>
|
||||
@@ -45,7 +46,7 @@ export const ErrorDisplay = ({
|
||||
</div>
|
||||
|
||||
<div className='flex gap-2'>
|
||||
{details && (
|
||||
{isExpandable && (
|
||||
<button className='text-red-600 hover:text-red-800 p-1 rounded'
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
>
|
||||
|
||||
@@ -3,33 +3,29 @@
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import React, { ButtonHTMLAttributes, FormEvent, FormHTMLAttributes, Fragment, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import React, { ButtonHTMLAttributes, FormEvent, FormHTMLAttributes, Fragment, KeyboardEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
|
||||
import { useAccessor, useSidebarState, useChatThreadsState, useChatThreadsStreamState, useUriState } from '../util/services.js';
|
||||
import { ChatMessage, CodeSelection, CodeStagingSelection } from '../../../chatThreadService.js';
|
||||
import { useAccessor, useSidebarState, useChatThreadsState, useChatThreadsStreamState, useUriState, useSettingsState } from '../util/services.js';
|
||||
import { ChatMessage, StagingSelectionItem } from '../../../chatThreadService.js';
|
||||
|
||||
import { BlockCode } from '../markdown/BlockCode.js';
|
||||
import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js';
|
||||
import { URI } from '../../../../../../../base/common/uri.js';
|
||||
import { EndOfLinePreference } from '../../../../../../../editor/common/model.js';
|
||||
import { IDisposable } from '../../../../../../../base/common/lifecycle.js';
|
||||
import { ErrorDisplay } from './ErrorDisplay.js';
|
||||
import { OnError, ServiceSendLLMMessageParams } from '../../../../../../../platform/void/common/llmMessageTypes.js';
|
||||
import { HistoryInputBox, InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js';
|
||||
import { TextAreaFns, VoidCodeEditorProps, VoidInputBox2 } from '../util/inputs.js';
|
||||
import { ModelDropdown, WarningBox } from '../void-settings-tsx/ModelDropdown.js';
|
||||
import { chat_systemMessage, chat_prompt } from '../../../prompt/prompts.js';
|
||||
import { ISidebarStateService } from '../../../sidebarStateService.js';
|
||||
import { ILLMMessageService } from '../../../../../../../platform/void/common/llmMessageService.js';
|
||||
import { IModelService } from '../../../../../../../editor/common/services/model.js';
|
||||
import { TextAreaFns, VoidInputBox2 } from '../util/inputs.js';
|
||||
import { ModelDropdown, } from '../void-settings-tsx/ModelDropdown.js';
|
||||
import { SidebarThreadSelector } from './SidebarThreadSelector.js';
|
||||
import { useScrollbarStyles } from '../util/useScrollbarStyles.js';
|
||||
import { VOID_CTRL_L_ACTION_ID } from '../../../actionIDs.js';
|
||||
import { ArrowBigLeftDash, CopyX, Delete, FileX2, SquareX, X } from 'lucide-react';
|
||||
import { filenameToVscodeLanguage } from '../../../helpers/detectLanguage.js';
|
||||
import { Pencil } from 'lucide-react'
|
||||
import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js';
|
||||
import { Pencil, X } from 'lucide-react';
|
||||
import { FeatureName, isFeatureNameDisabled } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js';
|
||||
import { WarningBox } from '../void-settings-tsx/WarningBox.js';
|
||||
import { ChatLocation } from '../../../searchAndReplaceService.js';
|
||||
|
||||
|
||||
|
||||
export const IconX = ({ size, className = '', ...props }: { size: number, className?: string } & React.SVGProps<SVGSVGElement>) => {
|
||||
@@ -141,6 +137,115 @@ export const IconLoading = ({ className = '' }: { className?: string }) => {
|
||||
|
||||
}
|
||||
|
||||
|
||||
interface VoidChatAreaProps {
|
||||
// Required
|
||||
children: React.ReactNode; // This will be the input component
|
||||
|
||||
// Form controls
|
||||
onSubmit: () => void;
|
||||
onAbort: () => void;
|
||||
isStreaming: boolean;
|
||||
isDisabled?: boolean;
|
||||
divRef?: React.RefObject<HTMLDivElement>;
|
||||
|
||||
// UI customization
|
||||
featureName: FeatureName;
|
||||
className?: string;
|
||||
showModelDropdown?: boolean;
|
||||
showSelections?: boolean;
|
||||
showProspectiveSelections?: boolean;
|
||||
|
||||
selections?: StagingSelectionItem[]
|
||||
setSelections?: (s: StagingSelectionItem[]) => void
|
||||
// selections?: any[];
|
||||
// onSelectionsChange?: (selections: any[]) => void;
|
||||
|
||||
onClickAnywhere?: () => void;
|
||||
// Optional close button
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export const VoidChatArea: React.FC<VoidChatAreaProps> = ({
|
||||
children,
|
||||
onSubmit,
|
||||
onAbort,
|
||||
onClose,
|
||||
onClickAnywhere,
|
||||
divRef,
|
||||
isStreaming = false,
|
||||
isDisabled = false,
|
||||
className = '',
|
||||
showModelDropdown = true,
|
||||
featureName,
|
||||
showSelections = false,
|
||||
showProspectiveSelections = true,
|
||||
selections,
|
||||
setSelections,
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
ref={divRef}
|
||||
className={`
|
||||
flex flex-col gap-1 p-2 relative input text-left shrink-0
|
||||
transition-all duration-200
|
||||
rounded-md
|
||||
bg-vscode-input-bg
|
||||
border border-void-border-3 focus-within:border-void-border-1 hover:border-void-border-1
|
||||
${className}
|
||||
`}
|
||||
onClick={(e) => {
|
||||
onClickAnywhere?.()
|
||||
}}
|
||||
>
|
||||
{/* Selections section */}
|
||||
{showSelections && selections && setSelections && (
|
||||
<SelectedFiles
|
||||
type='staging'
|
||||
selections={selections}
|
||||
setSelections={setSelections}
|
||||
showProspectiveSelections={showProspectiveSelections}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Input section */}
|
||||
<div className="relative w-full">
|
||||
{children}
|
||||
|
||||
{/* Close button (X) if onClose is provided */}
|
||||
{onClose && (
|
||||
<div className='absolute -top-1 -right-1 cursor-pointer z-1'>
|
||||
<IconX
|
||||
size={12}
|
||||
className="stroke-[2] opacity-80 text-void-fg-3 hover:brightness-95"
|
||||
onClick={onClose}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Bottom row */}
|
||||
<div className='flex flex-row justify-between items-end gap-1'>
|
||||
{showModelDropdown && (
|
||||
<div className='max-w-[150px] @@[&_select]:!void-border-none @@[&_select]:!void-outline-none flex-grow'
|
||||
onClick={(e) => { e.preventDefault(); e.stopPropagation() }}>
|
||||
<ModelDropdown featureName={featureName} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isStreaming ? (
|
||||
<ButtonStop onClick={onAbort} />
|
||||
) : (
|
||||
<ButtonSubmit
|
||||
onClick={onSubmit}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const useResizeObserver = () => {
|
||||
const ref = useRef(null);
|
||||
const [dimensions, setDimensions] = useState({ height: 0, width: 0 });
|
||||
@@ -261,8 +366,8 @@ const getBasename = (pathStr: string) => {
|
||||
|
||||
export const SelectedFiles = (
|
||||
{ type, selections, setSelections, showProspectiveSelections }:
|
||||
| { type: 'past', selections: CodeSelection[]; setSelections?: undefined, showProspectiveSelections?: undefined }
|
||||
| { type: 'staging', selections: CodeStagingSelection[]; setSelections: ((newSelections: CodeStagingSelection[]) => void), showProspectiveSelections?: boolean }
|
||||
| { type: 'past', selections: StagingSelectionItem[]; setSelections?: undefined, showProspectiveSelections?: undefined }
|
||||
| { type: 'staging', selections: StagingSelectionItem[]; setSelections: ((newSelections: StagingSelectionItem[]) => void), showProspectiveSelections?: boolean }
|
||||
) => {
|
||||
|
||||
// index -> isOpened
|
||||
@@ -287,11 +392,11 @@ export const SelectedFiles = (
|
||||
return withCurrent.slice(0, maxRecentUris)
|
||||
})
|
||||
}, [currentUri])
|
||||
let prospectiveSelections: CodeStagingSelection[] = []
|
||||
let prospectiveSelections: StagingSelectionItem[] = []
|
||||
if (type === 'staging' && showProspectiveSelections) { // handle prospective files
|
||||
// add a prospective file if type === 'staging' and if the user is in a file, and if the file is not selected yet
|
||||
prospectiveSelections = recentUris
|
||||
.filter(uri => !selections.find(s => s.range === null && s.fileURI.fsPath === uri.fsPath))
|
||||
.filter(uri => !selections.find(s => s.type === 'File' && s.fileURI.fsPath === uri.fsPath))
|
||||
.slice(0, maxProspectiveFiles)
|
||||
.map(uri => ({
|
||||
type: 'File',
|
||||
@@ -325,7 +430,7 @@ export const SelectedFiles = (
|
||||
>
|
||||
{/* selection summary */}
|
||||
<div // container for item and its delete button (if it's last)
|
||||
className='flex items-center gap-1 mr-0.5 mb-0.5'
|
||||
className='flex items-center gap-1 mr-0.5 my-0.5'
|
||||
>
|
||||
<div // styled summary box
|
||||
className={`flex items-center gap-0.5 relative
|
||||
@@ -339,7 +444,7 @@ export const SelectedFiles = (
|
||||
onClick={() => {
|
||||
if (isThisSelectionProspective) { // add prospective selection to selections
|
||||
if (type !== 'staging') return; // (never)
|
||||
setSelections([...selections, selection as CodeStagingSelection])
|
||||
setSelections([...selections, selection])
|
||||
|
||||
} else if (isThisSelectionAFile) { // open files
|
||||
commandService.executeCommand('vscode.open', selection.fileURI, {
|
||||
@@ -380,7 +485,7 @@ export const SelectedFiles = (
|
||||
</div>
|
||||
|
||||
{/* clear all selections button */}
|
||||
{type !== 'staging' || selections.length === 0 || i !== selections.length - 1
|
||||
{/* {type !== 'staging' || selections.length === 0 || i !== selections.length - 1
|
||||
? null
|
||||
: <div className={`flex items-center ${isThisSelectionOpened ? 'w-full' : ''}`}>
|
||||
<div
|
||||
@@ -402,7 +507,7 @@ export const SelectedFiles = (
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
} */}
|
||||
</div>
|
||||
{/* selection text */}
|
||||
{isThisSelectionOpened &&
|
||||
@@ -413,7 +518,7 @@ export const SelectedFiles = (
|
||||
}}
|
||||
>
|
||||
<BlockCode
|
||||
initValue={selection.selectionStr!}
|
||||
initValue={selection.selectionStr}
|
||||
language={filenameToVscodeLanguage(selection.fileURI.path)}
|
||||
maxHeight={200}
|
||||
showScrollbars={true}
|
||||
@@ -423,9 +528,8 @@ export const SelectedFiles = (
|
||||
</div>)
|
||||
|
||||
return <Fragment key={thisKey}>
|
||||
{selections.length > 0 && i === selections.length &&
|
||||
<div className='w-full'></div> // divider between `selections` and `prospectiveSelections`
|
||||
}
|
||||
{/* divider between `selections` and `prospectiveSelections` */}
|
||||
{/* {selections.length > 0 && i === selections.length && <div className='w-full'></div>} */}
|
||||
{selectionHTML}
|
||||
</Fragment>
|
||||
|
||||
@@ -438,92 +542,211 @@ export const SelectedFiles = (
|
||||
}
|
||||
|
||||
|
||||
type ChatBubbleMode = 'display' | 'edit'
|
||||
const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatMessage, messageIdx?: number, isLoading?: boolean, }) => {
|
||||
|
||||
const ChatBubble_ = ({ isEditMode, isLoading, children, role }: { role: ChatMessage['role'], children: React.ReactNode, isLoading: boolean, isEditMode: boolean }) => {
|
||||
const role = chatMessage.role
|
||||
|
||||
const accessor = useAccessor()
|
||||
const chatThreadsService = accessor.get('IChatThreadService')
|
||||
|
||||
// global state
|
||||
let isBeingEdited = false
|
||||
let setIsBeingEdited = (v: boolean) => { }
|
||||
let stagingSelections: StagingSelectionItem[] = []
|
||||
let setStagingSelections = (s: StagingSelectionItem[]) => { }
|
||||
|
||||
if (messageIdx !== undefined) {
|
||||
const [_state, _setState] = chatThreadsService._useCurrentMessageState(messageIdx)
|
||||
isBeingEdited = _state.isBeingEdited
|
||||
setIsBeingEdited = (v) => _setState({ isBeingEdited: v })
|
||||
stagingSelections = _state.stagingSelections
|
||||
setStagingSelections = (s) => { _setState({ stagingSelections: s }) }
|
||||
}
|
||||
|
||||
|
||||
// local state
|
||||
const mode: ChatBubbleMode = isBeingEdited ? 'edit' : 'display'
|
||||
const [isFocused, setIsFocused] = useState(false)
|
||||
const [isHovered, setIsHovered] = useState(false)
|
||||
const [isDisabled, setIsDisabled] = useState(false)
|
||||
const [textAreaRefState, setTextAreaRef] = useState<HTMLTextAreaElement | null>(null)
|
||||
const textAreaFnsRef = useRef<TextAreaFns | null>(null)
|
||||
// initialize on first render, and when edit was just enabled
|
||||
const _mustInitialize = useRef(true)
|
||||
const _justEnabledEdit = useRef(false)
|
||||
useEffect(() => {
|
||||
const canInitialize = role === 'user' && mode === 'edit' && textAreaRefState
|
||||
const shouldInitialize = _justEnabledEdit.current || _mustInitialize.current
|
||||
if (canInitialize && shouldInitialize) {
|
||||
setStagingSelections(chatMessage.selections || [])
|
||||
|
||||
if (textAreaFnsRef.current)
|
||||
textAreaFnsRef.current.setValue(chatMessage.displayContent || '')
|
||||
|
||||
textAreaRefState.focus();
|
||||
|
||||
_justEnabledEdit.current = false
|
||||
_mustInitialize.current = false
|
||||
}
|
||||
|
||||
}, [role, mode, _justEnabledEdit, textAreaRefState, textAreaFnsRef.current, _justEnabledEdit.current, _mustInitialize.current])
|
||||
const EditSymbol = mode === 'display' ? Pencil : X
|
||||
const onOpenEdit = () => {
|
||||
setIsBeingEdited(true)
|
||||
chatThreadsService.setFocusedMessageIdx(messageIdx)
|
||||
_justEnabledEdit.current = true
|
||||
}
|
||||
const onCloseEdit = () => {
|
||||
setIsFocused(false)
|
||||
setIsHovered(false)
|
||||
setIsBeingEdited(false)
|
||||
chatThreadsService.setFocusedMessageIdx(undefined)
|
||||
|
||||
}
|
||||
// set chat bubble contents
|
||||
let chatbubbleContents: React.ReactNode
|
||||
if (role === 'user') {
|
||||
if (mode === 'display') {
|
||||
chatbubbleContents = <>
|
||||
<SelectedFiles type='past' selections={chatMessage.selections || []} />
|
||||
{chatMessage.displayContent}
|
||||
</>
|
||||
}
|
||||
else if (mode === 'edit') {
|
||||
|
||||
const onSubmit = async () => {
|
||||
|
||||
if (isDisabled) return;
|
||||
if (!textAreaRefState) return;
|
||||
if (messageIdx === undefined) return;
|
||||
|
||||
// cancel any streams on this thread
|
||||
const thread = chatThreadsService.getCurrentThread()
|
||||
chatThreadsService.cancelStreaming(thread.id)
|
||||
|
||||
// reset state
|
||||
setIsBeingEdited(false)
|
||||
chatThreadsService.setFocusedMessageIdx(undefined)
|
||||
|
||||
// stream the edit
|
||||
const userMessage = textAreaRefState.value;
|
||||
await chatThreadsService.editUserMessageAndStreamResponse(userMessage, messageIdx)
|
||||
}
|
||||
|
||||
const onAbort = () => {
|
||||
const threadId = chatThreadsService.state.currentThreadId
|
||||
chatThreadsService.cancelStreaming(threadId)
|
||||
}
|
||||
|
||||
const onKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === 'Escape') {
|
||||
onCloseEdit()
|
||||
}
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
onSubmit()
|
||||
}
|
||||
}
|
||||
|
||||
if (!chatMessage.content && !isLoading) { // don't show if empty and not loading (if loading, want to show)
|
||||
return null
|
||||
}
|
||||
|
||||
chatbubbleContents = <>
|
||||
<VoidChatArea
|
||||
onSubmit={onSubmit}
|
||||
onAbort={onAbort}
|
||||
isStreaming={false}
|
||||
isDisabled={isDisabled}
|
||||
showSelections={true}
|
||||
showProspectiveSelections={false}
|
||||
featureName="Ctrl+L"
|
||||
selections={stagingSelections}
|
||||
setSelections={setStagingSelections}
|
||||
>
|
||||
<VoidInputBox2
|
||||
ref={setTextAreaRef}
|
||||
className='min-h-[81px] max-h-[500px] p-1'
|
||||
placeholder="Edit your message..."
|
||||
onChangeText={(text) => setIsDisabled(!text)}
|
||||
onFocus={() => {
|
||||
setIsFocused(true)
|
||||
chatThreadsService.setFocusedMessageIdx(messageIdx);
|
||||
}}
|
||||
onBlur={() => {
|
||||
setIsFocused(false)
|
||||
}}
|
||||
onKeyDown={onKeyDown}
|
||||
fnsRef={textAreaFnsRef}
|
||||
multiline={true}
|
||||
/>
|
||||
</VoidChatArea>
|
||||
</>
|
||||
}
|
||||
}
|
||||
else if (role === 'assistant') {
|
||||
const thread = chatThreadsService.getCurrentThread()
|
||||
|
||||
const chatLocation: ChatLocation = {
|
||||
threadId: thread.id,
|
||||
messageIdx: messageIdx!,
|
||||
}
|
||||
|
||||
chatbubbleContents = <ChatMarkdownRender string={chatMessage.displayContent ?? ''} chatLocation={chatLocation} />
|
||||
}
|
||||
|
||||
return <div
|
||||
// align chatbubble accoridng to role
|
||||
className={`
|
||||
relative
|
||||
${isEditMode ? 'px-2 w-full max-w-full'
|
||||
: role === 'user' ? `px-2 self-end w-fit max-w-full`
|
||||
relative
|
||||
${mode === 'edit' ? 'px-2 w-full max-w-full'
|
||||
: role === 'user' ? `px-2 self-end w-fit max-w-full whitespace-pre-wrap` // user words should be pre
|
||||
: role === 'assistant' ? `px-2 self-start w-full max-w-full` : ''
|
||||
}
|
||||
`}
|
||||
${role !== 'assistant' ? 'my-2' : ''}
|
||||
`}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
<div
|
||||
// style chatbubble according to role
|
||||
className={`
|
||||
text-left space-y-2 rounded-lg
|
||||
overflow-x-auto max-w-full
|
||||
${role === 'user' ? 'p-2 bg-void-bg-1 text-void-fg-1' : 'px-2'}
|
||||
`}
|
||||
text-left rounded-lg
|
||||
max-w-full
|
||||
${mode === 'edit' ? ''
|
||||
: role === 'user' ? 'p-2 bg-void-bg-1 text-void-fg-1 overflow-x-auto'
|
||||
: role === 'assistant' ? 'px-2 overflow-x-auto' : ''
|
||||
}
|
||||
`}
|
||||
>
|
||||
{children}
|
||||
{isLoading && <IconLoading className='opacity-50 text-sm' />}
|
||||
{chatbubbleContents}
|
||||
{isLoading && <IconLoading className='opacity-50 text-sm px-2' />}
|
||||
</div>
|
||||
|
||||
{/* edit button */}
|
||||
{/* {role === 'user' &&
|
||||
<Pencil
|
||||
size={16}
|
||||
{role === 'user' && <EditSymbol
|
||||
size={18}
|
||||
className={`
|
||||
absolute top-0 right-2
|
||||
absolute -top-1 right-1
|
||||
translate-x-0 -translate-y-0
|
||||
cursor-pointer z-1
|
||||
p-[2px]
|
||||
bg-void-bg-1 border border-void-border-1 rounded-md
|
||||
transition-opacity duration-200 ease-in-out
|
||||
${isHovered || (isFocused && mode === 'edit') ? 'opacity-100' : 'opacity-0'}
|
||||
`}
|
||||
onClick={() => { setIsEditMode(v => !v); }}
|
||||
/>
|
||||
} */}
|
||||
onClick={() => {
|
||||
if (mode === 'display') {
|
||||
onOpenEdit()
|
||||
} else if (mode === 'edit') {
|
||||
onCloseEdit()
|
||||
}
|
||||
}}
|
||||
/>}
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
const ChatBubble = ({ chatMessage, isLoading }: { chatMessage: ChatMessage, isLoading?: boolean, }) => {
|
||||
|
||||
const role = chatMessage.role
|
||||
|
||||
// edit mode state
|
||||
const [isEditMode, setIsEditMode] = useState(false)
|
||||
|
||||
|
||||
if (!chatMessage.content) { // don't show if empty
|
||||
return null
|
||||
}
|
||||
|
||||
let chatbubbleContents: React.ReactNode
|
||||
|
||||
if (role === 'user') {
|
||||
chatbubbleContents = <>
|
||||
<SelectedFiles type='past' selections={chatMessage.selections || []} />
|
||||
{chatMessage.displayContent}
|
||||
|
||||
{/* {!isEditMode ? chatMessage.displayContent : <></>} */}
|
||||
{/* edit mode content */}
|
||||
{/* TODO this should be the same input box as in the Sidebar */}
|
||||
{/* <textarea
|
||||
value={editModeText}
|
||||
className={`
|
||||
w-full max-w-full
|
||||
h-auto min-h-[81px] max-h-[500px]
|
||||
bg-void-bg-1 resize-none
|
||||
`}
|
||||
style={{ marginTop: 0 }}
|
||||
hidden={!isEditMode}
|
||||
/> */}
|
||||
|
||||
</>
|
||||
}
|
||||
else if (role === 'assistant') {
|
||||
chatbubbleContents = <ChatMarkdownRender string={chatMessage.displayContent ?? ''} />
|
||||
}
|
||||
|
||||
return <ChatBubble_ role={role} isEditMode={isEditMode} isLoading={!!isLoading}>
|
||||
{chatbubbleContents}
|
||||
</ChatBubble_>
|
||||
}
|
||||
|
||||
|
||||
export const SidebarChat = () => {
|
||||
|
||||
const textAreaRef = useRef<HTMLTextAreaElement | null>(null)
|
||||
@@ -532,15 +755,17 @@ export const SidebarChat = () => {
|
||||
const accessor = useAccessor()
|
||||
// const modelService = accessor.get('IModelService')
|
||||
const commandService = accessor.get('ICommandService')
|
||||
const chatThreadsService = accessor.get('IChatThreadService')
|
||||
|
||||
const settingsState = useSettingsState()
|
||||
// ----- HIGHER STATE -----
|
||||
// sidebar state
|
||||
const sidebarStateService = accessor.get('ISidebarStateService')
|
||||
useEffect(() => {
|
||||
const disposables: IDisposable[] = []
|
||||
disposables.push(
|
||||
sidebarStateService.onDidFocusChat(() => { textAreaRef.current?.focus() }),
|
||||
sidebarStateService.onDidBlurChat(() => { textAreaRef.current?.blur() })
|
||||
sidebarStateService.onDidFocusChat(() => { !chatThreadsService.isFocusingMessage() && textAreaRef.current?.focus() }),
|
||||
sidebarStateService.onDidBlurChat(() => { !chatThreadsService.isFocusingMessage() && textAreaRef.current?.blur() })
|
||||
)
|
||||
return () => disposables.forEach(d => d.dispose())
|
||||
}, [sidebarStateService, textAreaRef])
|
||||
@@ -549,11 +774,13 @@ export const SidebarChat = () => {
|
||||
|
||||
// threads state
|
||||
const chatThreadsState = useChatThreadsState()
|
||||
const chatThreadsService = accessor.get('IChatThreadService')
|
||||
|
||||
const currentThread = chatThreadsService.getCurrentThread()
|
||||
const previousMessages = currentThread?.messages ?? []
|
||||
const selections = chatThreadsState.currentStagingSelections
|
||||
|
||||
const [_state, _setState] = chatThreadsService._useCurrentThreadState()
|
||||
const selections = _state.stagingSelections
|
||||
const setSelections = (s: StagingSelectionItem[]) => { _setState({ stagingSelections: s }) }
|
||||
|
||||
// stream state
|
||||
const currThreadStreamState = useChatThreadsStreamState(chatThreadsState.currentThreadId)
|
||||
@@ -566,16 +793,17 @@ export const SidebarChat = () => {
|
||||
// state of current message
|
||||
const initVal = ''
|
||||
const [instructionsAreEmpty, setInstructionsAreEmpty] = useState(!initVal)
|
||||
const isDisabled = instructionsAreEmpty
|
||||
|
||||
const isDisabled = instructionsAreEmpty || !!isFeatureNameDisabled('Ctrl+L', settingsState)
|
||||
|
||||
const [sidebarRef, sidebarDimensions] = useResizeObserver()
|
||||
const [formRef, formDimensions] = useResizeObserver()
|
||||
const [chatAreaRef, chatAreaDimensions] = useResizeObserver()
|
||||
const [historyRef, historyDimensions] = useResizeObserver()
|
||||
|
||||
useScrollbarStyles(sidebarRef)
|
||||
|
||||
|
||||
const onSubmit = async () => {
|
||||
const onSubmit = useCallback(async () => {
|
||||
|
||||
if (isDisabled) return
|
||||
if (isStreaming) return
|
||||
@@ -584,11 +812,11 @@ export const SidebarChat = () => {
|
||||
const userMessage = textAreaRef.current?.value ?? ''
|
||||
await chatThreadsService.addUserMessageAndStreamResponse(userMessage)
|
||||
|
||||
chatThreadsService.setStaging([]) // clear staging
|
||||
setSelections([]) // clear staging
|
||||
textAreaFnsRef.current?.setValue('')
|
||||
textAreaRef.current?.focus() // focus input after submit
|
||||
|
||||
}
|
||||
}, [chatThreadsService, isDisabled, isStreaming, textAreaRef, textAreaFnsRef, selections, setSelections])
|
||||
|
||||
const onAbort = () => {
|
||||
const threadId = currentThread.id
|
||||
@@ -606,130 +834,100 @@ export const SidebarChat = () => {
|
||||
scrollContainerRef.current?.scrollTo({ top: 0, left: 0 })
|
||||
}, [isHistoryOpen, currentThread.id])
|
||||
|
||||
return <div
|
||||
ref={sidebarRef}
|
||||
className={`w-full h-full`}
|
||||
|
||||
const prevMessagesHTML = useMemo(() => {
|
||||
return previousMessages.map((message, i) =>
|
||||
<ChatBubble key={`${message.displayContent}-${i}`} chatMessage={message} messageIdx={i} />
|
||||
)
|
||||
}, [previousMessages])
|
||||
|
||||
|
||||
const threadSelector = <div ref={historyRef}
|
||||
className={`w-full h-auto ${isHistoryOpen ? '' : 'hidden'} ring-2 ring-widget-shadow ring-inset z-10`}
|
||||
>
|
||||
{/* thread selector */}
|
||||
<div ref={historyRef}
|
||||
className={`w-full h-auto ${isHistoryOpen ? '' : 'hidden'} ring-2 ring-widget-shadow ring-inset z-10`}
|
||||
>
|
||||
<SidebarThreadSelector />
|
||||
</div>
|
||||
|
||||
{/* previous messages + current stream */}
|
||||
<ScrollToBottomContainer
|
||||
scrollContainerRef={scrollContainerRef}
|
||||
className={`
|
||||
w-full h-auto
|
||||
flex flex-col gap-1
|
||||
overflow-x-hidden
|
||||
overflow-y-auto
|
||||
`}
|
||||
style={{ maxHeight: sidebarDimensions.height - historyDimensions.height - formDimensions.height - 36 }} // the height of the previousMessages is determined by all other heights
|
||||
>
|
||||
{/* previous messages */}
|
||||
{previousMessages.map((message, i) =>
|
||||
<ChatBubble key={i} chatMessage={message} />
|
||||
)}
|
||||
|
||||
{/* message stream */}
|
||||
<ChatBubble chatMessage={{ role: 'assistant', content: messageSoFar ?? '', displayContent: messageSoFar || null }} isLoading={isStreaming} />
|
||||
<SidebarThreadSelector />
|
||||
</div>
|
||||
|
||||
|
||||
{/* error message */}
|
||||
{latestError === undefined ? null :
|
||||
<div className='px-2'>
|
||||
<ErrorDisplay
|
||||
message={latestError.message}
|
||||
fullError={latestError.fullError}
|
||||
onDismiss={() => { chatThreadsService.dismissStreamError(currentThread.id) }}
|
||||
showDismiss={true}
|
||||
/>
|
||||
|
||||
<WarningBox className='text-sm my-2 pl-4' onClick={() => { commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID) }} text='Open settings' />
|
||||
</div>
|
||||
}
|
||||
const messagesHTML = <ScrollToBottomContainer
|
||||
scrollContainerRef={scrollContainerRef}
|
||||
className={`
|
||||
w-full h-auto
|
||||
flex flex-col
|
||||
overflow-x-hidden
|
||||
overflow-y-auto
|
||||
py-4
|
||||
${prevMessagesHTML.length === 0 && !messageSoFar ? 'hidden' : ''}
|
||||
`}
|
||||
style={{ maxHeight: sidebarDimensions.height - historyDimensions.height - chatAreaDimensions.height - 36 }} // the height of the previousMessages is determined by all other heights
|
||||
>
|
||||
{/* previous messages */}
|
||||
{prevMessagesHTML}
|
||||
|
||||
</ScrollToBottomContainer>
|
||||
{/* message stream */}
|
||||
<ChatBubble chatMessage={{ role: 'assistant', content: messageSoFar ?? '', displayContent: messageSoFar || null }} isLoading={isStreaming} />
|
||||
|
||||
|
||||
{/* input box */}
|
||||
<div // this div is used to position the input box properly
|
||||
className={`right-0 left-0 m-2 z-[999] overflow-hidden ${previousMessages.length > 0 ? 'absolute bottom-0' : ''}`}
|
||||
>
|
||||
<div
|
||||
ref={formRef}
|
||||
className={`
|
||||
flex flex-col gap-1 p-2 relative input text-left shrink-0
|
||||
transition-all duration-200
|
||||
rounded-md
|
||||
bg-vscode-input-bg
|
||||
max-h-[80vh] overflow-y-auto
|
||||
border border-void-border-3 focus-within:border-void-border-1 hover:border-void-border-1
|
||||
`}
|
||||
onClick={(e) => {
|
||||
textAreaRef.current?.focus()
|
||||
}}
|
||||
>
|
||||
{/* top row */}
|
||||
<>
|
||||
{/* selections */}
|
||||
<SelectedFiles type='staging' selections={selections || []} setSelections={chatThreadsService.setStaging.bind(chatThreadsService)} showProspectiveSelections={previousMessages.length === 0}/>
|
||||
</>
|
||||
|
||||
{/* middle row */}
|
||||
<div>
|
||||
|
||||
{/* text input */}
|
||||
<VoidInputBox2
|
||||
className='min-h-[81px] p-1'
|
||||
placeholder={`${keybindingString ? `${keybindingString} to select. ` : ''}Enter instructions...`}
|
||||
onChangeText={useCallback((newStr: string) => { setInstructionsAreEmpty(!newStr) }, [setInstructionsAreEmpty])}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
onSubmit()
|
||||
}
|
||||
}}
|
||||
ref={textAreaRef}
|
||||
fnsRef={textAreaFnsRef}
|
||||
multiline={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 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
|
||||
flex-grow
|
||||
'
|
||||
>
|
||||
<ModelDropdown featureName='Ctrl+L' />
|
||||
</div>
|
||||
|
||||
{/* submit / stop button */}
|
||||
{isStreaming ?
|
||||
// stop button
|
||||
<ButtonStop
|
||||
onClick={onAbort}
|
||||
/>
|
||||
:
|
||||
// submit button (up arrow)
|
||||
<ButtonSubmit
|
||||
onClick={onSubmit}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
|
||||
{/* error message */}
|
||||
{latestError === undefined ? null :
|
||||
<div className='px-2'>
|
||||
<ErrorDisplay
|
||||
message={latestError.message}
|
||||
fullError={latestError.fullError}
|
||||
onDismiss={() => { chatThreadsService.dismissStreamError(currentThread.id) }}
|
||||
showDismiss={true}
|
||||
/>
|
||||
|
||||
<WarningBox className='text-sm my-2 mx-4' onClick={() => { commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID) }} text='Open settings' />
|
||||
</div>
|
||||
</div >
|
||||
</div >
|
||||
}
|
||||
</ScrollToBottomContainer>
|
||||
|
||||
|
||||
const onChangeText = useCallback((newStr: string) => {
|
||||
setInstructionsAreEmpty(!newStr)
|
||||
}, [setInstructionsAreEmpty])
|
||||
const onKeyDown = useCallback((e: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
onSubmit()
|
||||
}
|
||||
}, [onSubmit])
|
||||
const inputForm = <div className={`right-0 left-0 m-2 z-[999] overflow-hidden ${previousMessages.length > 0 ? 'absolute bottom-0' : ''}`}>
|
||||
<VoidChatArea
|
||||
divRef={chatAreaRef}
|
||||
onSubmit={onSubmit}
|
||||
onAbort={onAbort}
|
||||
isStreaming={isStreaming}
|
||||
isDisabled={isDisabled}
|
||||
showSelections={true}
|
||||
showProspectiveSelections={prevMessagesHTML.length === 0}
|
||||
selections={selections}
|
||||
setSelections={setSelections}
|
||||
onClickAnywhere={() => { textAreaRef.current?.focus() }}
|
||||
featureName="Ctrl+L"
|
||||
>
|
||||
<VoidInputBox2
|
||||
className='min-h-[81px] p-1'
|
||||
placeholder={`${keybindingString ? `${keybindingString} to select. ` : ''}Enter instructions...`}
|
||||
onChangeText={onChangeText}
|
||||
onKeyDown={onKeyDown}
|
||||
onFocus={() => { chatThreadsService.setFocusedMessageIdx(undefined) }}
|
||||
ref={textAreaRef}
|
||||
fnsRef={textAreaFnsRef}
|
||||
multiline={true}
|
||||
/>
|
||||
</VoidChatArea>
|
||||
</div>
|
||||
|
||||
return <div ref={sidebarRef} className={`w-full h-full`}>
|
||||
{threadSelector}
|
||||
|
||||
{messagesHTML}
|
||||
|
||||
{inputForm}
|
||||
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -104,6 +104,7 @@ export const SidebarThreadSelector = () => {
|
||||
flex items-center
|
||||
`}
|
||||
onClick={() => chatThreadsService.switchToThread(pastThread.id)}
|
||||
onDoubleClick={() => sidebarStateService.setState({ isHistoryOpen: false })}
|
||||
title={new Date(pastThread.createdAt).toLocaleString()}
|
||||
>
|
||||
<div className='truncate'>{`${firstMsg}`}</div>
|
||||
|
||||
@@ -15,6 +15,7 @@ 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
|
||||
@@ -57,9 +58,11 @@ type InputBox2Props = {
|
||||
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, onChangeText }, ref) {
|
||||
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)
|
||||
@@ -113,6 +116,9 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
|
||||
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}`}
|
||||
@@ -296,9 +302,10 @@ export const VoidCheckBox = ({ label, value, onClick, className }: { label: stri
|
||||
}
|
||||
|
||||
|
||||
export const VoidCustomSelectBox = <T extends any>({
|
||||
|
||||
export const VoidCustomDropdownBox = <T extends any>({
|
||||
options,
|
||||
selectedOption: selectedOption_,
|
||||
selectedOption,
|
||||
onChangeOption,
|
||||
getOptionDropdownName,
|
||||
getOptionDisplayName,
|
||||
@@ -306,11 +313,10 @@ export const VoidCustomSelectBox = <T extends any>({
|
||||
className,
|
||||
arrowTouchesText = true,
|
||||
matchInputWidth = false,
|
||||
isMenuPositionFixed = true,
|
||||
gap = 0,
|
||||
}: {
|
||||
options: T[];
|
||||
selectedOption?: T;
|
||||
selectedOption: T | undefined;
|
||||
onChangeOption: (newValue: T) => void;
|
||||
getOptionDropdownName: (option: T) => string;
|
||||
getOptionDisplayName: (option: T) => string;
|
||||
@@ -318,104 +324,94 @@ export const VoidCustomSelectBox = <T extends any>({
|
||||
className?: string;
|
||||
arrowTouchesText?: boolean;
|
||||
matchInputWidth?: boolean;
|
||||
isMenuPositionFixed?: boolean;
|
||||
gap?: number;
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [readyToShow, setReadyToShow] = useState(false);
|
||||
const [position, setPosition] = useState({ top: 0, left: 0, width: 0 });
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const buttonRef = useRef<HTMLButtonElement | null>(null);
|
||||
const measureRef = useRef<HTMLDivElement | null>(null);
|
||||
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',
|
||||
|
||||
// if the selected option is null, use the 0th option as the selected, and set the option to options[0]
|
||||
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[0]) return
|
||||
if (!selectedOption_) {
|
||||
onChangeOption(options[0]);
|
||||
}
|
||||
}, [selectedOption_, options])
|
||||
const selectedOption = !selectedOption_ ? options[0] : selectedOption_
|
||||
|
||||
|
||||
const updatePosition = useCallback(() => {
|
||||
if (!buttonRef.current || !containerRef.current || !measureRef.current) return;
|
||||
|
||||
const buttonRect = buttonRef.current.getBoundingClientRect();
|
||||
const containerRect = containerRef.current.getBoundingClientRect();
|
||||
const containerWidth = containerRef.current.offsetWidth;
|
||||
const viewportHeight = window.innerHeight;
|
||||
const spaceBelow = viewportHeight - buttonRect.bottom;
|
||||
const spaceNeeded = options.length * 28;
|
||||
const showAbove = spaceBelow < spaceNeeded && buttonRect.top > spaceBelow;
|
||||
|
||||
// Calculate the menu width
|
||||
let menuWidth = matchInputWidth ? containerWidth : buttonRect.width;
|
||||
|
||||
// If not matchInputWidth, calculate content width from measurement div
|
||||
if (!matchInputWidth) {
|
||||
const contentWidth = measureRef.current.offsetWidth;
|
||||
menuWidth = Math.max(buttonRect.width, contentWidth);
|
||||
}
|
||||
|
||||
if (isMenuPositionFixed) {
|
||||
// Fixed positioning (relative to viewport)
|
||||
setPosition({
|
||||
top: showAbove
|
||||
? buttonRect.top - spaceNeeded
|
||||
: buttonRect.bottom + gap,
|
||||
left: buttonRect.left,
|
||||
width: menuWidth,
|
||||
});
|
||||
} else {
|
||||
// Absolute positioning (relative to parent container)
|
||||
setPosition({
|
||||
top: showAbove
|
||||
? -(spaceNeeded + gap)
|
||||
: buttonRect.height + gap,
|
||||
left: 0,
|
||||
width: menuWidth,
|
||||
});
|
||||
}
|
||||
|
||||
setReadyToShow(true);
|
||||
}, [gap, matchInputWidth, options.length, isMenuPositionFixed]);
|
||||
if (options.length === 0) return
|
||||
if (selectedOption) return
|
||||
onChangeOption(options[0])
|
||||
}, [selectedOption, onChangeOption, options])
|
||||
|
||||
// Handle clicks outside
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setReadyToShow(false);
|
||||
updatePosition();
|
||||
window.addEventListener('scroll', updatePosition, true);
|
||||
window.addEventListener('resize', updatePosition);
|
||||
if (!isOpen) return;
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('scroll', updatePosition, true);
|
||||
window.removeEventListener('resize', updatePosition);
|
||||
};
|
||||
} else {
|
||||
setReadyToShow(false);
|
||||
}
|
||||
}, [isOpen, updatePosition]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
if (isOpen) {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}
|
||||
}, [isOpen]);
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, [isOpen, refs.floating, refs.reference]);
|
||||
|
||||
if (!selectedOption)
|
||||
return null
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={`inline-block relative ${className}`}
|
||||
>
|
||||
<div className={`inline-block relative ${className}`}>
|
||||
{/* Hidden measurement div */}
|
||||
<div
|
||||
ref={measureRef}
|
||||
@@ -433,11 +429,9 @@ export const VoidCustomSelectBox = <T extends any>({
|
||||
{/* Select Button */}
|
||||
<button
|
||||
type='button'
|
||||
ref={buttonRef}
|
||||
ref={refs.setReference}
|
||||
className="flex items-center h-4 bg-transparent whitespace-nowrap hover:brightness-90 w-full"
|
||||
onClick={() => {
|
||||
setIsOpen(!isOpen);
|
||||
}}
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
<span className={`max-w-[120px] truncate ${arrowTouchesText ? 'mr-1' : ''}`}>
|
||||
{getOptionDisplayName(selectedOption)}
|
||||
@@ -458,13 +452,20 @@ export const VoidCustomSelectBox = <T extends any>({
|
||||
</button>
|
||||
|
||||
{/* Dropdown Menu */}
|
||||
{isOpen && readyToShow && (
|
||||
{isOpen && (
|
||||
<div
|
||||
className={`${isMenuPositionFixed ? 'fixed' : 'absolute'} z-10 bg-void-bg-1 border-void-border-1 border overflow-hidden rounded shadow-lg`}
|
||||
ref={refs.setFloating}
|
||||
className="z-10 bg-void-bg-1 border-void-border-1 border overflow-hidden rounded shadow-lg"
|
||||
style={{
|
||||
top: position.top,
|
||||
left: position.left,
|
||||
width: position.width,
|
||||
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) => {
|
||||
@@ -687,6 +688,8 @@ export const VoidCodeEditor = ({ initValue, language, maxHeight, showScrollbars
|
||||
// maxColumn: 0,
|
||||
},
|
||||
|
||||
hover: { enabled: false },
|
||||
|
||||
selectionHighlight: false, // highlights whole words
|
||||
renderLineHighlight: 'none',
|
||||
|
||||
|
||||
@@ -5,14 +5,14 @@
|
||||
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { ThreadStreamState, ThreadsState } from '../../../chatThreadService.js'
|
||||
import { RefreshableProviderName, SettingsOfProvider } from '../../../../../../../platform/void/common/voidSettingsTypes.js'
|
||||
import { RefreshableProviderName, SettingsOfProvider } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js'
|
||||
import { IDisposable } from '../../../../../../../base/common/lifecycle.js'
|
||||
import { VoidSidebarState } from '../../../sidebarStateService.js'
|
||||
import { VoidSettingsState } from '../../../../../../../platform/void/common/voidSettingsService.js'
|
||||
import { VoidSettingsState } from '../../../../../../../workbench/contrib/void/common/voidSettingsService.js'
|
||||
import { ColorScheme } from '../../../../../../../platform/theme/common/theme.js'
|
||||
import { VoidUriState } from '../../../voidUriStateService.js';
|
||||
import { VoidQuickEditState } from '../../../quickEditStateService.js'
|
||||
import { RefreshModelStateOfProvider } from '../../../../../../../platform/void/common/refreshModelService.js'
|
||||
import { RefreshModelStateOfProvider } from '../../../../../../../workbench/contrib/void/common/refreshModelService.js'
|
||||
|
||||
|
||||
|
||||
@@ -25,9 +25,9 @@ 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 '../../../../../../../platform/void/common/llmMessageService.js';
|
||||
import { IRefreshModelService } from '../../../../../../../platform/void/common/refreshModelService.js';
|
||||
import { IVoidSettingsService } from '../../../../../../../platform/void/common/voidSettingsService.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 { IInlineDiffsService } from '../../../inlineDiffsService.js';
|
||||
import { IVoidUriStateService } from '../../../voidUriStateService.js';
|
||||
import { IQuickEditStateService } from '../../../quickEditStateService.js';
|
||||
@@ -46,7 +46,7 @@ import { IKeybindingService } from '../../../../../../../platform/keybinding/com
|
||||
import { IEnvironmentService } from '../../../../../../../platform/environment/common/environment.js'
|
||||
import { IConfigurationService } from '../../../../../../../platform/configuration/common/configuration.js'
|
||||
import { IPathService } from '../../../../../../../workbench/services/path/common/pathService.js'
|
||||
import { IMetricsService } from '../../../../../../../platform/void/common/metricsService.js'
|
||||
import { IMetricsService } from '../../../../../../../workbench/contrib/void/common/metricsService.js'
|
||||
|
||||
|
||||
|
||||
@@ -288,6 +288,17 @@ export const useChatThreadsState = () => {
|
||||
return () => { chatThreadsStateListeners.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
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export const useScrollbarStyles = (containerRef: React.MutableRefObject<HTMLDivElement | null>) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
@@ -12,90 +11,121 @@ export const useScrollbarStyles = (containerRef: React.MutableRefObject<HTMLDivE
|
||||
'[class*="overflow-y-auto"]'
|
||||
].join(',');
|
||||
|
||||
// Get all matching elements within the container, including the container itself
|
||||
const scrollElements = [
|
||||
...(containerRef.current.matches(overflowSelector) ? [containerRef.current] : []),
|
||||
...Array.from(containerRef.current.querySelectorAll(overflowSelector))
|
||||
];
|
||||
// Function to initialize scrollbar styles for elements
|
||||
const initializeScrollbarStyles = () => {
|
||||
// Get all matching elements within the container, including the container itself
|
||||
const scrollElements = [
|
||||
...(containerRef.current?.matches(overflowSelector) ? [containerRef.current] : []),
|
||||
...Array.from(containerRef.current?.querySelectorAll(overflowSelector) || [])
|
||||
];
|
||||
|
||||
// Apply styles and listeners to each scroll element
|
||||
scrollElements.forEach(element => {
|
||||
// Add the scrollable class directly to the overflow element
|
||||
element.classList.add('void-scrollable-element');
|
||||
|
||||
let fadeTimeout: NodeJS.Timeout | null = null;
|
||||
let fadeInterval: NodeJS.Timeout | null = null;
|
||||
|
||||
const fadeIn = () => {
|
||||
if (fadeInterval) clearInterval(fadeInterval);
|
||||
|
||||
let step = 0;
|
||||
fadeInterval = setInterval(() => {
|
||||
if (step <= 10) {
|
||||
element.classList.remove(`show-scrollbar-${step - 1}`);
|
||||
element.classList.add(`show-scrollbar-${step}`);
|
||||
step++;
|
||||
} else {
|
||||
clearInterval(fadeInterval!);
|
||||
}
|
||||
}, 10);
|
||||
};
|
||||
|
||||
const fadeOut = () => {
|
||||
if (fadeInterval) clearInterval(fadeInterval);
|
||||
|
||||
let step = 10;
|
||||
fadeInterval = setInterval(() => {
|
||||
if (step >= 0) {
|
||||
element.classList.remove(`show-scrollbar-${step + 1}`);
|
||||
element.classList.add(`show-scrollbar-${step}`);
|
||||
step--;
|
||||
} else {
|
||||
clearInterval(fadeInterval!);
|
||||
}
|
||||
}, 60);
|
||||
};
|
||||
|
||||
const onMouseEnter = () => {
|
||||
if (fadeTimeout) clearTimeout(fadeTimeout);
|
||||
if (fadeInterval) clearInterval(fadeInterval);
|
||||
fadeIn();
|
||||
};
|
||||
|
||||
const onMouseLeave = () => {
|
||||
if (fadeTimeout) clearTimeout(fadeTimeout);
|
||||
fadeTimeout = setTimeout(() => {
|
||||
fadeOut();
|
||||
}, 10);
|
||||
};
|
||||
|
||||
element.addEventListener('mouseenter', onMouseEnter);
|
||||
element.addEventListener('mouseleave', onMouseLeave);
|
||||
|
||||
// Store cleanup function
|
||||
const cleanup = () => {
|
||||
element.removeEventListener('mouseenter', onMouseEnter);
|
||||
element.removeEventListener('mouseleave', onMouseLeave);
|
||||
if (fadeTimeout) clearTimeout(fadeTimeout);
|
||||
if (fadeInterval) clearInterval(fadeInterval);
|
||||
element.classList.remove('void-scrollable-element');
|
||||
// Remove any remaining show-scrollbar classes
|
||||
for (let i = 0; i <= 10; i++) {
|
||||
element.classList.remove(`show-scrollbar-${i}`);
|
||||
}
|
||||
};
|
||||
|
||||
// Store the cleanup function on the element for later use
|
||||
(element as any).__scrollbarCleanup = cleanup;
|
||||
});
|
||||
|
||||
return () => {
|
||||
// Clean up all scroll elements
|
||||
// Clean up existing elements first
|
||||
scrollElements.forEach(element => {
|
||||
if ((element as any).__scrollbarCleanup) {
|
||||
(element as any).__scrollbarCleanup();
|
||||
}
|
||||
});
|
||||
|
||||
// Apply styles and listeners to each scroll element
|
||||
scrollElements.forEach(element => {
|
||||
// Add the scrollable class directly to the overflow element
|
||||
element.classList.add('void-scrollable-element');
|
||||
|
||||
let fadeTimeout: NodeJS.Timeout | null = null;
|
||||
let fadeInterval: NodeJS.Timeout | null = null;
|
||||
|
||||
const fadeIn = () => {
|
||||
if (fadeInterval) clearInterval(fadeInterval);
|
||||
|
||||
let step = 0;
|
||||
fadeInterval = setInterval(() => {
|
||||
if (step <= 10) {
|
||||
element.classList.remove(`show-scrollbar-${step - 1}`);
|
||||
element.classList.add(`show-scrollbar-${step}`);
|
||||
step++;
|
||||
} else {
|
||||
clearInterval(fadeInterval!);
|
||||
}
|
||||
}, 10);
|
||||
};
|
||||
|
||||
const fadeOut = () => {
|
||||
if (fadeInterval) clearInterval(fadeInterval);
|
||||
|
||||
let step = 10;
|
||||
fadeInterval = setInterval(() => {
|
||||
if (step >= 0) {
|
||||
element.classList.remove(`show-scrollbar-${step + 1}`);
|
||||
element.classList.add(`show-scrollbar-${step}`);
|
||||
step--;
|
||||
} else {
|
||||
clearInterval(fadeInterval!);
|
||||
}
|
||||
}, 60);
|
||||
};
|
||||
|
||||
const onMouseEnter = () => {
|
||||
if (fadeTimeout) clearTimeout(fadeTimeout);
|
||||
if (fadeInterval) clearInterval(fadeInterval);
|
||||
fadeIn();
|
||||
};
|
||||
|
||||
const onMouseLeave = () => {
|
||||
if (fadeTimeout) clearTimeout(fadeTimeout);
|
||||
fadeTimeout = setTimeout(() => {
|
||||
fadeOut();
|
||||
}, 10);
|
||||
};
|
||||
|
||||
element.addEventListener('mouseenter', onMouseEnter);
|
||||
element.addEventListener('mouseleave', onMouseLeave);
|
||||
|
||||
// Store cleanup function
|
||||
const cleanup = () => {
|
||||
element.removeEventListener('mouseenter', onMouseEnter);
|
||||
element.removeEventListener('mouseleave', onMouseLeave);
|
||||
if (fadeTimeout) clearTimeout(fadeTimeout);
|
||||
if (fadeInterval) clearInterval(fadeInterval);
|
||||
element.classList.remove('void-scrollable-element');
|
||||
// Remove any remaining show-scrollbar classes
|
||||
for (let i = 0; i <= 10; i++) {
|
||||
element.classList.remove(`show-scrollbar-${i}`);
|
||||
}
|
||||
};
|
||||
|
||||
// Store the cleanup function on the element for later use
|
||||
(element as any).__scrollbarCleanup = cleanup;
|
||||
});
|
||||
};
|
||||
|
||||
// Initialize for the first time
|
||||
initializeScrollbarStyles();
|
||||
|
||||
// Set up mutation observer
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
initializeScrollbarStyles();
|
||||
});
|
||||
|
||||
// Start observing the container for child changes
|
||||
observer.observe(containerRef.current, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
// Your existing cleanup code...
|
||||
if (containerRef.current) {
|
||||
const scrollElements = [
|
||||
...(containerRef.current.matches(overflowSelector) ? [containerRef.current] : []),
|
||||
...Array.from(containerRef.current.querySelectorAll(overflowSelector))
|
||||
];
|
||||
scrollElements.forEach(element => {
|
||||
if ((element as any).__scrollbarCleanup) {
|
||||
(element as any).__scrollbarCleanup();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}, [containerRef]);
|
||||
};
|
||||
|
||||
+21
-40
@@ -4,15 +4,14 @@
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { FeatureName, featureNames, ModelSelection, modelSelectionsEqual, ProviderName, providerNames } from '../../../../../../../platform/void/common/voidSettingsTypes.js'
|
||||
import { FeatureName, featureNames, isFeatureNameDisabled, ModelSelection, modelSelectionsEqual, ProviderName, providerNames, SettingsOfProvider } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js'
|
||||
import { useSettingsState, useRefreshModelState, useAccessor } from '../util/services.js'
|
||||
import { _VoidSelectBox, VoidCustomSelectBox } from '../util/inputs.js'
|
||||
import { _VoidSelectBox, VoidCustomDropdownBox } from '../util/inputs.js'
|
||||
import { SelectBox } from '../../../../../../../base/browser/ui/selectBox/selectBox.js'
|
||||
import { IconWarning } from '../sidebar-tsx/SidebarChat.js'
|
||||
import { VOID_OPEN_SETTINGS_ACTION_ID, VOID_TOGGLE_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js'
|
||||
import { ModelOption } from '../../../../../../../platform/void/common/voidSettingsService.js'
|
||||
|
||||
|
||||
import { ModelOption } from '../../../../../../../workbench/contrib/void/common/voidSettingsService.js'
|
||||
import { WarningBox } from './WarningBox.js'
|
||||
|
||||
const optionsEqual = (m1: ModelOption[], m2: ModelOption[]) => {
|
||||
if (m1.length !== m2.length) return false
|
||||
@@ -27,22 +26,21 @@ const ModelSelectBox = ({ options, featureName }: { options: ModelOption[], feat
|
||||
const voidSettingsService = accessor.get('IVoidSettingsService')
|
||||
|
||||
const selection = voidSettingsService.state.modelSelectionOfFeature[featureName]
|
||||
const selectedOption = selection ? voidSettingsService.state._modelOptions.find(v => modelSelectionsEqual(v.selection, selection)) : options[0]
|
||||
const selectedOption = selection ? voidSettingsService.state._modelOptions.find(v => modelSelectionsEqual(v.selection, selection))! : options[0]
|
||||
|
||||
const onChangeOption = useCallback((newOption: ModelOption) => {
|
||||
voidSettingsService.setModelSelectionOfFeature(featureName, newOption.selection)
|
||||
}, [voidSettingsService, featureName])
|
||||
|
||||
return <VoidCustomSelectBox
|
||||
return <VoidCustomDropdownBox
|
||||
options={options}
|
||||
selectedOption={selectedOption}
|
||||
onChangeOption={onChangeOption}
|
||||
getOptionDisplayName={(option) => option.selection.modelName}
|
||||
getOptionDropdownName={(option) => option.name}
|
||||
getOptionsEqual={(a, b) => optionsEqual([a], [b])}
|
||||
className={`text-xs text-void-fg-3 px-1`}
|
||||
className='text-xs text-void-fg-3 px-1'
|
||||
matchInputWidth={false}
|
||||
isMenuPositionFixed={featureName === 'Ctrl+K' ? false : true}
|
||||
/>
|
||||
}
|
||||
// const ModelSelectBox = ({ options, featureName }: { options: ModelOption[], featureName: FeatureName }) => {
|
||||
@@ -76,10 +74,13 @@ const ModelSelectBox = ({ options, featureName }: { options: ModelOption[], feat
|
||||
// />
|
||||
// }
|
||||
|
||||
const MemoizedModelSelectBox = ({ featureName }: { featureName: FeatureName }) => {
|
||||
|
||||
|
||||
const MemoizedModelDropdown = ({ featureName }: { featureName: FeatureName }) => {
|
||||
const settingsState = useSettingsState()
|
||||
const oldOptionsRef = useRef<ModelOption[]>([])
|
||||
const [memoizedOptions, setMemoizedOptions] = useState(oldOptionsRef.current)
|
||||
|
||||
useEffect(() => {
|
||||
const oldOptions = oldOptionsRef.current
|
||||
const newOptions = settingsState._modelOptions
|
||||
@@ -93,30 +94,6 @@ const MemoizedModelSelectBox = ({ featureName }: { featureName: FeatureName }) =
|
||||
|
||||
}
|
||||
|
||||
export const WarningBox = ({ text, onClick, className }: { text: string; onClick?: () => void; className?: string }) => {
|
||||
|
||||
return <div
|
||||
className={`
|
||||
text-void-warning brightness-90 opacity-90
|
||||
text-xs text-ellipsis
|
||||
${onClick ? `hover:brightness-75 transition-all duration-200 cursor-pointer` : ''}
|
||||
flex items-center flex-nowrap
|
||||
${className}
|
||||
`}
|
||||
onClick={onClick}
|
||||
>
|
||||
<IconWarning
|
||||
size={14}
|
||||
className='mr-1'
|
||||
/>
|
||||
<span>{text}</span>
|
||||
</div>
|
||||
// return <VoidSelectBox
|
||||
// options={[{ text: 'Please add a model!', value: null }]}
|
||||
// onChangeSelection={() => { }}
|
||||
// />
|
||||
}
|
||||
|
||||
export const ModelDropdown = ({ featureName }: { featureName: FeatureName }) => {
|
||||
const settingsState = useSettingsState()
|
||||
|
||||
@@ -125,10 +102,14 @@ export const ModelDropdown = ({ featureName }: { featureName: FeatureName }) =>
|
||||
|
||||
const openSettings = () => { commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID); };
|
||||
|
||||
return <>
|
||||
{settingsState._modelOptions.length === 0 ?
|
||||
<WarningBox onClick={openSettings} text='Provider required' />
|
||||
: <MemoizedModelSelectBox featureName={featureName} />
|
||||
}
|
||||
</>
|
||||
const isDisabled = isFeatureNameDisabled(featureName, settingsState)
|
||||
if (isDisabled)
|
||||
return <WarningBox onClick={openSettings} text={
|
||||
isDisabled === 'needToEnableModel' ? 'Enable a model'
|
||||
: isDisabled === 'addModel' ? 'Add a model'
|
||||
: (isDisabled === 'addProvider' || isDisabled === 'notFilledIn' || isDisabled === 'providerNotAutoDetected') ? 'Provider required'
|
||||
: 'Provider required'
|
||||
} />
|
||||
|
||||
return <MemoizedModelDropdown featureName={featureName} />
|
||||
}
|
||||
|
||||
@@ -5,17 +5,18 @@
|
||||
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js'
|
||||
import { ProviderName, SettingName, displayInfoOfSettingName, providerNames, VoidModelInfo, globalSettingNames, customSettingNamesOfProvider, RefreshableProviderName, refreshableProviderNames, displayInfoOfProviderName, defaultProviderSettings, nonlocalProviderNames, localProviderNames, GlobalSettingName } from '../../../../../../../platform/void/common/voidSettingsTypes.js'
|
||||
import { ProviderName, SettingName, displayInfoOfSettingName, providerNames, VoidModelInfo, globalSettingNames, customSettingNamesOfProvider, RefreshableProviderName, refreshableProviderNames, displayInfoOfProviderName, defaultProviderSettings, nonlocalProviderNames, localProviderNames, GlobalSettingName, featureNames, displayInfoOfFeatureName, isProviderNameDisabled } from '../../../../common/voidSettingsTypes.js'
|
||||
import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js'
|
||||
import { VoidButton, VoidCheckBox, VoidCustomSelectBox, VoidInputBox, VoidInputBox2, VoidSwitch } from '../util/inputs.js'
|
||||
import { VoidButton, VoidCheckBox, VoidCustomDropdownBox, VoidInputBox, VoidInputBox2, VoidSwitch } from '../util/inputs.js'
|
||||
import { useAccessor, useIsDark, useRefreshModelListener, useRefreshModelState, useSettingsState } from '../util/services.js'
|
||||
import { X, RefreshCw, Loader2, Check, MoveRight } from 'lucide-react'
|
||||
import { useScrollbarStyles } from '../util/useScrollbarStyles.js'
|
||||
import { isWindows, isLinux, isMacintosh } from '../../../../../../../base/common/platform.js'
|
||||
import { URI } from '../../../../../../../base/common/uri.js'
|
||||
import { env } from '../../../../../../../base/common/process.js'
|
||||
import { WarningBox } from './ModelDropdown.js'
|
||||
import { ModelDropdown } from './ModelDropdown.js'
|
||||
import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js'
|
||||
import { WarningBox } from './WarningBox.js'
|
||||
|
||||
const SubtleButton = ({ onClick, text, icon, disabled }: { onClick: () => void, text: string, icon: React.ReactNode, disabled: boolean }) => {
|
||||
|
||||
@@ -79,7 +80,7 @@ const RefreshableModels = () => {
|
||||
|
||||
|
||||
const buttons = refreshableProviderNames.map(providerName => {
|
||||
if (!settingsState.settingsOfProvider[providerName]._enabled) return null
|
||||
if (!settingsState.settingsOfProvider[providerName]._didFillInProviderSettings) return null
|
||||
return <div key={providerName} className='pb-4'>
|
||||
<RefreshModelButton providerName={providerName} />
|
||||
</div>
|
||||
@@ -112,7 +113,7 @@ const AddModelMenu = ({ onSubmit }: { onSubmit: () => void }) => {
|
||||
<div className='flex items-center gap-4'>
|
||||
|
||||
{/* provider */}
|
||||
<VoidCustomSelectBox
|
||||
<VoidCustomDropdownBox
|
||||
options={providerNames}
|
||||
selectedOption={providerName}
|
||||
onChangeOption={(pn) => setProviderName(pn)}
|
||||
@@ -199,7 +200,7 @@ export const ModelDump = () => {
|
||||
for (let providerName of providerNames) {
|
||||
const providerSettings = settingsState.settingsOfProvider[providerName]
|
||||
// if (!providerSettings.enabled) continue
|
||||
modelDump.push(...providerSettings.models.map(model => ({ ...model, providerName, providerEnabled: !!providerSettings._enabled })))
|
||||
modelDump.push(...providerSettings.models.map(model => ({ ...model, providerName, providerEnabled: !!providerSettings._didFillInProviderSettings })))
|
||||
}
|
||||
|
||||
// sort by hidden
|
||||
@@ -223,7 +224,6 @@ export const ModelDump = () => {
|
||||
<div className={`flex-grow flex items-center gap-4`}>
|
||||
<span className='w-full max-w-32'>{isNewProviderName ? displayInfoOfProviderName(providerName).title : ''}</span>
|
||||
<span className='w-fit truncate'>{modelName}</span>
|
||||
{/* <span>{`${modelName} (${providerName})`}</span> */}
|
||||
</div>
|
||||
{/* right part is anything that fits */}
|
||||
<div className='flex items-center gap-4'>
|
||||
@@ -260,7 +260,6 @@ const ProviderSetting = ({ providerName, settingName }: { providerName: Provider
|
||||
|
||||
const accessor = useAccessor()
|
||||
const voidSettingsService = accessor.get('IVoidSettingsService')
|
||||
const voidMetricsService = accessor.get('IMetricsService')
|
||||
|
||||
let weChangedTextRef = false
|
||||
|
||||
@@ -284,25 +283,8 @@ const ProviderSetting = ({ providerName, settingName }: { providerName: Provider
|
||||
weChangedTextRef = true
|
||||
instance.value = stateVal as string
|
||||
weChangedTextRef = false
|
||||
|
||||
const isEverySettingPresent = Object.keys(defaultProviderSettings[providerName]).every(key => {
|
||||
return !!settingsAtProvider[key as keyof typeof settingsAtProvider]
|
||||
})
|
||||
|
||||
const shouldEnable = isEverySettingPresent && !settingsAtProvider._enabled // enable if all settings are present and not already enabled
|
||||
const shouldDisable = !isEverySettingPresent && settingsAtProvider._enabled
|
||||
|
||||
if (shouldEnable) {
|
||||
voidSettingsService.setSettingOfProvider(providerName, '_enabled', true)
|
||||
voidMetricsService.capture('Enable Provider', { providerName })
|
||||
}
|
||||
|
||||
if (shouldDisable) {
|
||||
voidSettingsService.setSettingOfProvider(providerName, '_enabled', false)
|
||||
voidMetricsService.capture('Disable Provider', { providerName })
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
syncInstance()
|
||||
const disposable = voidSettingsService.onDidChangeState(syncInstance)
|
||||
return [disposable]
|
||||
@@ -318,7 +300,10 @@ const ProviderSetting = ({ providerName, settingName }: { providerName: Provider
|
||||
}
|
||||
|
||||
const SettingsForProvider = ({ providerName }: { providerName: ProviderName }) => {
|
||||
// const voidSettingsState = useSettingsState()
|
||||
const voidSettingsState = useSettingsState()
|
||||
|
||||
const needsModel = isProviderNameDisabled(providerName, voidSettingsState) === 'addModel'
|
||||
|
||||
// const accessor = useAccessor()
|
||||
// const voidSettingsService = accessor.get('IVoidSettingsService')
|
||||
|
||||
@@ -349,6 +334,12 @@ const SettingsForProvider = ({ providerName }: { providerName: ProviderName }) =
|
||||
{settingNames.map((settingName, i) => {
|
||||
return <ProviderSetting key={settingName} providerName={providerName} settingName={settingName} />
|
||||
})}
|
||||
|
||||
{needsModel ?
|
||||
providerName === 'ollama' ?
|
||||
<WarningBox text={`Please install an Ollama model. We'll auto-detect it.`} />
|
||||
: <WarningBox text={`Please add a model for ${providerTitle} below (Models).`} />
|
||||
: null}
|
||||
</div>
|
||||
</div >
|
||||
}
|
||||
@@ -392,7 +383,7 @@ export const AIInstructionsBox = () => {
|
||||
const voidSettingsService = accessor.get('IVoidSettingsService')
|
||||
const voidSettingsState = useSettingsState()
|
||||
return <VoidInputBox2
|
||||
className='min-h-[81px] p-3 rounded-sm'
|
||||
className='min-h-[81px] p-3 rounded-sm'
|
||||
initValue={voidSettingsState.globalSettings.aiInstructions}
|
||||
placeholder={`Do not change my indentation or delete my comments. When writing TS or JS, do not add ;'s. Respond to all queries in French. `}
|
||||
multiline
|
||||
@@ -597,6 +588,17 @@ const GeneralTab = () => {
|
||||
<AIInstructionsBox />
|
||||
</div>
|
||||
|
||||
<div className='mt-12'>
|
||||
<h2 className={`text-3xl mb-2`}>Model Selection</h2>
|
||||
{featureNames.map(featureName =>
|
||||
<div key={featureName}
|
||||
className='mb-2'
|
||||
>
|
||||
<h4 className={`text-void-fg-3`}>{displayInfoOfFeatureName(featureName)}</h4>
|
||||
<ModelDropdown featureName={featureName} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { IconWarning } from '../sidebar-tsx/SidebarChat.js';
|
||||
|
||||
|
||||
export const WarningBox = ({ text, onClick, className }: { text: string; onClick?: () => void; className?: string }) => {
|
||||
|
||||
return <div
|
||||
className={`
|
||||
text-void-warning brightness-90 opacity-90 w-fit
|
||||
text-xs text-ellipsis
|
||||
${onClick ? `hover:brightness-75 transition-all duration-200 cursor-pointer` : ''}
|
||||
flex items-center flex-nowrap
|
||||
${className}
|
||||
`}
|
||||
onClick={onClick}
|
||||
>
|
||||
<IconWarning
|
||||
size={14}
|
||||
className='mr-1'
|
||||
/>
|
||||
<span>{text}</span>
|
||||
</div>
|
||||
// return <VoidSelectBox
|
||||
// options={[{ text: 'Please add a model!', value: null }]}
|
||||
// onChangeSelection={() => { }}
|
||||
// />
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Emitter, Event } from '../../../../base/common/event.js';
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
|
||||
|
||||
|
||||
export type ChatLocation = {
|
||||
threadId: string;
|
||||
messageIdx: number;
|
||||
}
|
||||
|
||||
export type ApplyBoxLocation = ChatLocation & { codeblockId: string }
|
||||
|
||||
export const getApplyBoxId = ({ threadId, messageIdx, codeblockId }: ApplyBoxLocation) => {
|
||||
return `${threadId}-${messageIdx}-${codeblockId}}`
|
||||
}
|
||||
|
||||
export type SearchAndReplaceBlock = {
|
||||
search: string;
|
||||
replace: string;
|
||||
}
|
||||
|
||||
// service that manages state
|
||||
export type ApplyState = {
|
||||
[applyBoxId: string]: {
|
||||
searchAndReplaceBlocks: SearchAndReplaceBlock;
|
||||
}
|
||||
}
|
||||
|
||||
// the purpose of this service is to generate search and replace blocks for a given codeblock `codeblockId` and on a file `fileName` and version `fileVersion`
|
||||
|
||||
export interface IFastApplyService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
// readonly state: ApplyState; // readonly to the user
|
||||
// setState(newState: Partial<ApplyState>): void;
|
||||
// onDidChangeState: Event<void>;
|
||||
}
|
||||
|
||||
export const IVoidFastApplyService = createDecorator<IFastApplyService>('voidFastApplyService');
|
||||
class VoidFastApplyService extends Disposable implements IFastApplyService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
static readonly ID = 'voidFastApplyService';
|
||||
|
||||
private readonly _onDidChangeState = new Emitter<void>();
|
||||
readonly onDidChangeState: Event<void> = this._onDidChangeState.event;
|
||||
|
||||
|
||||
// state
|
||||
// state: ApplyState
|
||||
|
||||
constructor(
|
||||
) {
|
||||
super()
|
||||
|
||||
// initial state
|
||||
// this.state = { currentUri: undefined }
|
||||
}
|
||||
|
||||
setState(newState: Partial<ApplyState>) {
|
||||
|
||||
// this.state = { ...this.state, ...newState }
|
||||
this._onDidChangeState.fire()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
registerSingleton(IVoidFastApplyService, VoidFastApplyService, InstantiationType.Eager);
|
||||
@@ -11,13 +11,13 @@ import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js
|
||||
|
||||
import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';
|
||||
import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js';
|
||||
import { CodeStagingSelection, IChatThreadService } from './chatThreadService.js';
|
||||
import { StagingSelectionItem, IChatThreadService } from './chatThreadService.js';
|
||||
|
||||
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
|
||||
import { IRange } from '../../../../editor/common/core/range.js';
|
||||
import { ITextModel } from '../../../../editor/common/model.js';
|
||||
import { VOID_VIEW_CONTAINER_ID, VOID_VIEW_ID } from './sidebarPane.js';
|
||||
import { IMetricsService } from '../../../../platform/void/common/metricsService.js';
|
||||
import { IMetricsService } from '../common/metricsService.js';
|
||||
import { ISidebarStateService } from './sidebarStateService.js';
|
||||
import { ICommandService } from '../../../../platform/commands/common/commands.js';
|
||||
import { VOID_TOGGLE_SETTINGS_ACTION_ID } from './voidSettingsPane.js';
|
||||
@@ -67,6 +67,13 @@ const getContentInRange = (model: ITextModel, range: IRange | null) => {
|
||||
}
|
||||
|
||||
|
||||
const findMatchingStagingIndex = (currentSelections: StagingSelectionItem[] | undefined, newSelection: StagingSelectionItem) => {
|
||||
return currentSelections?.findIndex(s =>
|
||||
s.fileURI.fsPath === newSelection.fileURI.fsPath
|
||||
&& s.range?.startLineNumber === newSelection.range?.startLineNumber
|
||||
&& s.range?.endLineNumber === newSelection.range?.endLineNumber
|
||||
)
|
||||
}
|
||||
|
||||
const VOID_OPEN_SIDEBAR_ACTION_ID = 'void.sidebar.open'
|
||||
registerAction2(class extends Action2 {
|
||||
@@ -112,7 +119,7 @@ registerAction2(class extends Action2 {
|
||||
|
||||
const selectionStr = getContentInRange(model, selectionRange)
|
||||
|
||||
const selection: CodeStagingSelection = !selectionRange || !selectionStr || (selectionRange.startLineNumber > selectionRange.endLineNumber) ? {
|
||||
const selection: StagingSelectionItem = !selectionRange || !selectionStr || (selectionRange.startLineNumber > selectionRange.endLineNumber) ? {
|
||||
type: 'File',
|
||||
fileURI: model.uri,
|
||||
selectionStr: null,
|
||||
@@ -124,26 +131,37 @@ registerAction2(class extends Action2 {
|
||||
range: selectionRange,
|
||||
}
|
||||
|
||||
// add selection to staging
|
||||
// update the staging selections
|
||||
const chatThreadService = accessor.get(IChatThreadService)
|
||||
const currentStaging = chatThreadService.state.currentStagingSelections
|
||||
const currentStagingEltIdx = currentStaging?.findIndex(s =>
|
||||
s.fileURI.fsPath === model.uri.fsPath
|
||||
&& s.range?.startLineNumber === selection.range?.startLineNumber
|
||||
&& s.range?.endLineNumber === selection.range?.endLineNumber
|
||||
)
|
||||
|
||||
// if matches with existing selection, overwrite
|
||||
if (currentStagingEltIdx !== undefined && currentStagingEltIdx !== -1) {
|
||||
chatThreadService.setStaging([
|
||||
...currentStaging!.slice(0, currentStagingEltIdx),
|
||||
const focusedMessageIdx = chatThreadService.getFocusedMessageIdx()
|
||||
|
||||
// set the selections to the proper value
|
||||
let selections: StagingSelectionItem[] = []
|
||||
let setSelections = (s: StagingSelectionItem[]) => { }
|
||||
|
||||
if (focusedMessageIdx === undefined) {
|
||||
const [state, setState] = chatThreadService._useCurrentThreadState()
|
||||
selections = state.stagingSelections
|
||||
setSelections = (s) => setState({ stagingSelections: s })
|
||||
} else {
|
||||
const [state, setState] = chatThreadService._useCurrentMessageState(focusedMessageIdx)
|
||||
selections = state.stagingSelections
|
||||
setSelections = (s) => setState({ stagingSelections: s })
|
||||
}
|
||||
|
||||
// if matches with existing selection, overwrite (since text may change)
|
||||
const matchingStagingEltIdx = findMatchingStagingIndex(selections, selection)
|
||||
if (matchingStagingEltIdx !== undefined && matchingStagingEltIdx !== -1) {
|
||||
setSelections([
|
||||
...selections!.slice(0, matchingStagingEltIdx),
|
||||
selection,
|
||||
...currentStaging!.slice(currentStagingEltIdx + 1, Infinity)
|
||||
...selections!.slice(matchingStagingEltIdx + 1, Infinity)
|
||||
])
|
||||
}
|
||||
// if no match, add
|
||||
// if no match, add it
|
||||
else {
|
||||
chatThreadService.setStaging([...(currentStaging ?? []), selection])
|
||||
setSelections([...(selections ?? []), selection])
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -21,6 +21,10 @@ import './chatThreadService.js'
|
||||
// register Autocomplete
|
||||
import './autocompleteService.js'
|
||||
|
||||
// register Context services
|
||||
// import './contextGatheringService.js'
|
||||
// import './contextUserChangesService.js'
|
||||
|
||||
// settings pane
|
||||
import './voidSettingsPane.js'
|
||||
|
||||
@@ -29,3 +33,26 @@ import './media/void.css'
|
||||
|
||||
// update (frontend part, also see platform/)
|
||||
import './voidUpdateActions.js'
|
||||
|
||||
|
||||
|
||||
|
||||
// ---------- common (unclear if these actually need to be imported, because they're already imported wherever they're used) ----------
|
||||
|
||||
// llmMessage
|
||||
import '../common/llmMessageService.js'
|
||||
|
||||
// voidSettings
|
||||
import '../common/voidSettingsService.js'
|
||||
|
||||
// refreshModel
|
||||
import '../common/refreshModelService.js'
|
||||
|
||||
// metrics
|
||||
import '../common/metricsService.js'
|
||||
|
||||
// updates
|
||||
import '../common/voidUpdateService.js'
|
||||
|
||||
// tools
|
||||
import '../common/toolsService.js'
|
||||
|
||||
@@ -8,7 +8,7 @@ import { EditorInput } from '../../../common/editor/editorInput.js';
|
||||
import * as nls from '../../../../nls.js';
|
||||
import { EditorExtensions } from '../../../common/editor.js';
|
||||
import { EditorPane } from '../../../browser/parts/editor/editorPane.js';
|
||||
import { IEditorGroup } from '../../../services/editor/common/editorGroupsService.js';
|
||||
import { IEditorGroup, IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js';
|
||||
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
|
||||
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
|
||||
import { IStorageService } from '../../../../platform/storage/common/storage.js';
|
||||
@@ -35,7 +35,7 @@ class VoidSettingsInput extends EditorInput {
|
||||
static readonly ID: string = 'workbench.input.void.settings';
|
||||
|
||||
static readonly RESOURCE = URI.from({ // I think this scheme is invalid, it just shuts up TS
|
||||
scheme: 'void', // Custom scheme for our editor
|
||||
scheme: 'void', // Custom scheme for our editor (try Schemas.https)
|
||||
path: 'settings'
|
||||
})
|
||||
readonly resource = VoidSettingsInput.RESOURCE;
|
||||
@@ -141,18 +141,27 @@ registerAction2(class extends Action2 {
|
||||
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const editorService = accessor.get(IEditorService);
|
||||
const editorGroupService = accessor.get(IEditorGroupsService);
|
||||
|
||||
const instantiationService = accessor.get(IInstantiationService);
|
||||
|
||||
// close all instances if found
|
||||
const openEditors = editorService.findEditors(VoidSettingsInput.RESOURCE);
|
||||
if (openEditors.length > 0) {
|
||||
await editorService.closeEditors(openEditors);
|
||||
// if is open, close it
|
||||
const openEditors = editorService.findEditors(VoidSettingsInput.RESOURCE); // should only have 0 or 1 elements...
|
||||
if (openEditors.length !== 0) {
|
||||
const openEditor = openEditors[0].editor
|
||||
const isCurrentlyOpen = editorService.activeEditor?.resource?.fsPath === openEditor.resource?.fsPath
|
||||
if (isCurrentlyOpen)
|
||||
await editorService.closeEditors(openEditors)
|
||||
else
|
||||
await editorGroupService.activeGroup.openEditor(openEditor)
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// else open it
|
||||
const input = instantiationService.createInstance(VoidSettingsInput);
|
||||
await editorService.openEditor(input);
|
||||
|
||||
await editorGroupService.activeGroup.openEditor(input);
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@ import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js
|
||||
import { localize2 } from '../../../../nls.js';
|
||||
import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js';
|
||||
import { INotificationService } from '../../../../platform/notification/common/notification.js';
|
||||
import { IMetricsService } from '../../../../platform/void/common/metricsService.js';
|
||||
import { IVoidUpdateService } from '../../../../platform/void/common/voidUpdateService.js';
|
||||
import { IMetricsService } from '../common/metricsService.js';
|
||||
import { IVoidUpdateService } from '../common/voidUpdateService.js';
|
||||
import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js';
|
||||
|
||||
|
||||
@@ -53,10 +53,11 @@ registerAction2(class extends Action2 {
|
||||
const notifService = accessor.get(INotificationService)
|
||||
const metricsService = accessor.get(IMetricsService)
|
||||
|
||||
metricsService.capture('Void Update Manual: Checking...', {})
|
||||
const res = await voidUpdateService.check()
|
||||
if (!res) { notifyErrChecking(notifService); metricsService.capture('Void Update: Error', {}) }
|
||||
else if (res.hasUpdate) { notifyYesUpdate(notifService, res.message); metricsService.capture('Void Update: Yes', {}) }
|
||||
else if (!res.hasUpdate) { notifyNoUpdate(notifService); metricsService.capture('Void Update: No', {}) }
|
||||
if (!res) { notifyErrChecking(notifService); metricsService.capture('Void Update Manual: Error', { res }) }
|
||||
else if (res.hasUpdate) { notifyYesUpdate(notifService, res.message); metricsService.capture('Void Update Manual: Yes', { res }) }
|
||||
else if (!res.hasUpdate) { notifyNoUpdate(notifService); metricsService.capture('Void Update Manual: No', { res }) }
|
||||
}
|
||||
})
|
||||
|
||||
@@ -65,23 +66,27 @@ class VoidUpdateWorkbenchContribution extends Disposable implements IWorkbenchCo
|
||||
static readonly ID = 'workbench.contrib.void.voidUpdate'
|
||||
constructor(
|
||||
@IVoidUpdateService private readonly voidUpdateService: IVoidUpdateService,
|
||||
@INotificationService private readonly notifService: INotificationService,
|
||||
@IMetricsService private readonly metricsService: IMetricsService,
|
||||
@INotificationService private readonly notifService: INotificationService,
|
||||
) {
|
||||
super()
|
||||
|
||||
// on mount
|
||||
setTimeout(async () => {
|
||||
const autoCheck = async () => {
|
||||
this.metricsService.capture('Void Update Startup: Checking...', {})
|
||||
const res = await this.voidUpdateService.check()
|
||||
if (!res) { notifyErrChecking(this.notifService); this.metricsService.capture('Void Update Startup: Error', { res }) }
|
||||
else if (res.hasUpdate) { notifyYesUpdate(this.notifService, res.message); this.metricsService.capture('Void Update Startup: Yes', { res }) }
|
||||
else if (!res.hasUpdate) { this.metricsService.capture('Void Update Startup: No', { res }) } // display nothing if up to date
|
||||
}
|
||||
|
||||
const notifService = this.notifService
|
||||
const metricsService = this.metricsService
|
||||
// check once 5 seconds after mount
|
||||
|
||||
if (!res) { notifyErrChecking(notifService); metricsService.capture('Void Update Startup: Error', {}) }
|
||||
else if (res.hasUpdate) { notifyYesUpdate(this.notifService, res.message); metricsService.capture('Void Update Startup: Yes', {}) }
|
||||
else if (!res.hasUpdate) { metricsService.capture('Void Update Startup: No', {}) } // display nothing if up to date
|
||||
const initId = setTimeout(() => autoCheck(), 5 * 1000)
|
||||
this._register({ dispose: () => clearTimeout(initId) })
|
||||
|
||||
// check every 3 hours
|
||||
const intervalId = setInterval(() => autoCheck(), 3 * 60 * 60 * 1000)
|
||||
this._register({ dispose: () => clearInterval(intervalId) })
|
||||
|
||||
}, 5 * 1000)
|
||||
}
|
||||
}
|
||||
registerWorkbenchContribution2(VoidUpdateWorkbenchContribution.ID, VoidUpdateWorkbenchContribution, WorkbenchPhase.BlockRestore);
|
||||
|
||||
+38
-22
@@ -3,15 +3,17 @@
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, ServiceSendLLMMessageParams, MainLLMMessageParams, MainLLMMessageAbortParams, ServiceModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, MainModelListParams, OllamaModelResponse, OpenaiCompatibleModelResponse, } from './llmMessageTypes.js';
|
||||
import { IChannel } from '../../../base/parts/ipc/common/ipc.js';
|
||||
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 { 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 { IVoidSettingsService } from './voidSettingsService.js';
|
||||
import { displayInfoOfProviderName, isFeatureNameDisabled } from './voidSettingsTypes.js';
|
||||
// import { INotificationService } from '../../notification/common/notification.js';
|
||||
|
||||
// calls channel to implement features
|
||||
@@ -90,36 +92,49 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
|
||||
const { onText, onFinalMessage, onError, ...proxyParams } = params;
|
||||
const { useProviderFor: featureName } = proxyParams
|
||||
|
||||
// end early if no provider
|
||||
// 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)
|
||||
const modelSelection = this.voidSettingsService.state.modelSelectionOfFeature[featureName]
|
||||
if (modelSelection === null) {
|
||||
onError({ message: 'Please add a Provider in Settings!', fullError: null })
|
||||
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 })
|
||||
return null
|
||||
}
|
||||
|
||||
const { providerName, modelName } = modelSelection
|
||||
|
||||
const aiInstructions = this.voidSettingsService.state.globalSettings.aiInstructions
|
||||
if (aiInstructions)
|
||||
proxyParams.messages.unshift({ role: 'system', content: aiInstructions })
|
||||
|
||||
// add state for request id
|
||||
const requestId_ = generateUuid();
|
||||
this.onTextHooks_llm[requestId_] = onText
|
||||
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,
|
||||
requestId: requestId_,
|
||||
aiInstructions,
|
||||
requestId,
|
||||
providerName,
|
||||
modelName,
|
||||
settingsOfProvider,
|
||||
} satisfies MainLLMMessageParams);
|
||||
} satisfies MainSendLLMMessageParams);
|
||||
|
||||
return requestId_
|
||||
return requestId
|
||||
}
|
||||
|
||||
|
||||
@@ -146,6 +161,7 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
|
||||
} satisfies MainModelListParams<OllamaModelResponse>)
|
||||
}
|
||||
|
||||
|
||||
openAICompatibleList = (params: ServiceModelListParams<OpenaiCompatibleModelResponse>) => {
|
||||
const { onSuccess, onError, ...proxyParams } = params
|
||||
|
||||
+65
-49
@@ -3,8 +3,7 @@
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IRange } from '../../../editor/common/core/range'
|
||||
import { ProviderName, SettingsOfProvider } from './voidSettingsTypes.js'
|
||||
import { FeatureName, ProviderName, SettingsOfProvider } from './voidSettingsTypes.js'
|
||||
|
||||
|
||||
export const errorDetails = (fullError: Error | null): string | null => {
|
||||
@@ -12,6 +11,7 @@ export const errorDetails = (fullError: Error | null): string | 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') {
|
||||
@@ -25,71 +25,94 @@ 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 LLMMessage = {
|
||||
export type LLMChatMessage = {
|
||||
role: 'system' | 'user' | 'assistant';
|
||||
content: string;
|
||||
}
|
||||
|
||||
export type ServiceSendLLMFeatureParams = {
|
||||
useProviderFor: 'Ctrl+K';
|
||||
range: IRange;
|
||||
} | {
|
||||
useProviderFor: 'Ctrl+L';
|
||||
} | {
|
||||
useProviderFor: 'Autocomplete';
|
||||
range: IRange;
|
||||
export type _InternalLLMChatMessage = {
|
||||
role: 'user' | 'assistant';
|
||||
content: string;
|
||||
}
|
||||
|
||||
// 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;
|
||||
type _InternalSendFIMMessage = {
|
||||
prefix: string;
|
||||
suffix: string;
|
||||
stopTokens: string[];
|
||||
}
|
||||
|
||||
type SendLLMType = {
|
||||
messagesType: 'chatMessages';
|
||||
messages: LLMChatMessage[];
|
||||
} | {
|
||||
messagesType: 'FIMMessage';
|
||||
messages: _InternalSendFIMMessage;
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
_setAborter: (aborter: () => void) => void;
|
||||
}) => void
|
||||
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
|
||||
|
||||
// service -> main -> internal -> event (back to main)
|
||||
// (browser)
|
||||
@@ -102,13 +125,6 @@ export type _InternalSendLLMMessageFnType = (params: {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// These are from 'ollama' SDK
|
||||
interface OllamaModelDetails {
|
||||
parent_model: string;
|
||||
+8
-8
@@ -3,14 +3,14 @@
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { createDecorator } from '../../instantiation/common/instantiation.js';
|
||||
import { ProxyChannel } from '../../../base/parts/ipc/common/ipc.js';
|
||||
import { IMainProcessService } from '../../ipc/common/mainProcessService.js';
|
||||
import { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js';
|
||||
import { Action2, registerAction2 } from '../../actions/common/actions.js';
|
||||
import { localize2 } from '../../../nls.js';
|
||||
import { ServicesAccessor } from '../../../editor/browser/editorExtensions.js';
|
||||
import { INotificationService } from '../../notification/common/notification.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { ProxyChannel } from '../../../../base/parts/ipc/common/ipc.js';
|
||||
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { IMainProcessService } from '../../../../platform/ipc/common/mainProcessService.js';
|
||||
import { localize2 } from '../../../../nls.js';
|
||||
import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js';
|
||||
import { registerAction2, Action2 } from '../../../../platform/actions/common/actions.js';
|
||||
import { INotificationService } from '../../../../platform/notification/common/notification.js';
|
||||
|
||||
export interface IMetricsService {
|
||||
readonly _serviceBrand: undefined;
|
||||
+8
-8
@@ -3,14 +3,14 @@
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { createDecorator } from '../../instantiation/common/instantiation.js';
|
||||
import { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js';
|
||||
import { IVoidSettingsService } from './voidSettingsService.js';
|
||||
import { ILLMMessageService } from './llmMessageService.js';
|
||||
import { Emitter, Event } from '../../../base/common/event.js';
|
||||
import { Disposable, 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';
|
||||
|
||||
|
||||
|
||||
@@ -44,8 +44,8 @@ export type RefreshModelStateOfProvider = Record<RefreshableProviderName, Refres
|
||||
|
||||
|
||||
const refreshBasedOn: { [k in RefreshableProviderName]: (keyof SettingsOfProvider[k])[] } = {
|
||||
ollama: ['_enabled', 'endpoint'],
|
||||
// openAICompatible: ['_enabled', 'endpoint', 'apiKey'],
|
||||
ollama: ['_didFillInProviderSettings', 'endpoint'],
|
||||
// openAICompatible: ['_didFillInProviderSettings', 'endpoint', 'apiKey'],
|
||||
}
|
||||
const REFRESH_INTERVAL = 5_000
|
||||
// const COOLDOWN_TIMEOUT = 300
|
||||
@@ -95,7 +95,7 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
|
||||
|
||||
for (const providerName of refreshableProviderNames) {
|
||||
|
||||
// const { _enabled: enabled } = this.voidSettingsService.state.settingsOfProvider[providerName]
|
||||
// const { '_didFillInProviderSettings': enabled } = this.voidSettingsService.state.settingsOfProvider[providerName]
|
||||
this.startRefreshingModels(providerName, autoOptions)
|
||||
|
||||
// every time providerName.enabled changes, refresh models too, like a useEffect
|
||||
@@ -175,7 +175,7 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
|
||||
{ enableProviderOnSuccess: options.enableProviderOnSuccess, hideRefresh: options.doNotFire }
|
||||
)
|
||||
|
||||
if (options.enableProviderOnSuccess) this.voidSettingsService.setSettingOfProvider(providerName, '_enabled', true)
|
||||
if (options.enableProviderOnSuccess) this.voidSettingsService.setSettingOfProvider(providerName, '_didFillInProviderSettings', true)
|
||||
|
||||
this._setRefreshState(providerName, 'finished', options)
|
||||
autoPoll()
|
||||
@@ -0,0 +1,182 @@
|
||||
import { CancellationToken } from '../../../../base/common/cancellation.js'
|
||||
import { URI } from '../../../../base/common/uri.js'
|
||||
import { IFileService, IFileStat } from '../../../../platform/files/common/files.js'
|
||||
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js'
|
||||
import { createDecorator, IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'
|
||||
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'
|
||||
import { _VSReadFileRaw } from '../../../../workbench/contrib/void/browser/helpers/readFile.js'
|
||||
import { QueryBuilder } from '../../../../workbench/services/search/common/queryBuilder.js'
|
||||
import { ISearchService } from '../../../../workbench/services/search/common/search.js'
|
||||
|
||||
|
||||
// tool use for AI
|
||||
|
||||
|
||||
|
||||
// we do this using Anthropic's style and convert to OpenAI style later
|
||||
export type InternalToolInfo = {
|
||||
description: string,
|
||||
params: {
|
||||
[paramName: string]: { type: string, description: string | undefined } // name -> type
|
||||
},
|
||||
required: string[], // required paramNames
|
||||
}
|
||||
|
||||
// helper
|
||||
const pagination = {
|
||||
desc: `Very large results may be paginated (indicated in the result). Pagination fails gracefully if out of bounds or invalid page number.`,
|
||||
param: { pageNumber: { type: 'number', description: 'The page number (optional, default is 1).' }, }
|
||||
} as const
|
||||
|
||||
export const contextTools = {
|
||||
read_file: {
|
||||
description: 'Returns file contents of a given URI.',
|
||||
params: {
|
||||
uri: { type: 'string', description: undefined },
|
||||
},
|
||||
required: ['uri'],
|
||||
},
|
||||
|
||||
list_dir: {
|
||||
description: `Returns all file names and folder names in a given URI. ${pagination.desc}`,
|
||||
params: {
|
||||
uri: { type: 'string', description: undefined },
|
||||
...pagination.param
|
||||
},
|
||||
required: ['uri'],
|
||||
},
|
||||
|
||||
pathname_search: {
|
||||
description: `Returns all pathnames that match a given grep query. You should use this when looking for a file with a specific name or path. This does NOT search file content. ${pagination.desc}`,
|
||||
params: {
|
||||
query: { type: 'string', description: undefined },
|
||||
...pagination.param,
|
||||
},
|
||||
required: ['query']
|
||||
},
|
||||
|
||||
search: {
|
||||
description: `Returns all code excerpts containing the given string or grep query. This does NOT search pathname. As a follow-up, you may want to use read_file to view the full file contents of the results. ${pagination.desc}`,
|
||||
params: {
|
||||
query: { type: 'string', description: undefined },
|
||||
...pagination.param,
|
||||
},
|
||||
required: ['query'],
|
||||
},
|
||||
|
||||
// semantic_search: {
|
||||
// description: 'Searches files semantically for the given string query.',
|
||||
// // RAG
|
||||
// },
|
||||
|
||||
} as const satisfies { [name: string]: InternalToolInfo }
|
||||
|
||||
export type ContextToolName = keyof typeof contextTools
|
||||
type ContextToolParamNames<T extends ContextToolName> = keyof typeof contextTools[T]['params']
|
||||
type ContextToolParams<T extends ContextToolName> = { [paramName in ContextToolParamNames<T>]: unknown }
|
||||
|
||||
type AllContextToolCallFns = {
|
||||
[ToolName in ContextToolName]: ((p: (ContextToolParams<ToolName>)) => Promise<string>)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// TODO check to make sure in workspace
|
||||
// TODO check to make sure is not gitignored
|
||||
|
||||
|
||||
async function generateDirectoryTreeMd(fileService: IFileService, rootURI: URI): Promise<string> {
|
||||
let output = ''
|
||||
function traverseChildren(children: IFileStat[], depth: number) {
|
||||
const indentation = ' '.repeat(depth);
|
||||
for (const child of children) {
|
||||
output += `${indentation}- ${child.name}\n`;
|
||||
traverseChildren(child.children ?? [], depth + 1);
|
||||
}
|
||||
}
|
||||
const stat = await fileService.resolve(rootURI, { resolveMetadata: false });
|
||||
|
||||
// kickstart recursion
|
||||
output += `${stat.name}\n`;
|
||||
traverseChildren(stat.children ?? [], 1);
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
|
||||
const validateURI = (uriStr: unknown) => {
|
||||
if (typeof uriStr !== 'string') throw new Error('(uri was not a string)')
|
||||
const uri = URI.file(uriStr)
|
||||
return uri
|
||||
}
|
||||
|
||||
export interface IToolService {
|
||||
readonly _serviceBrand: undefined;
|
||||
callContextTool: <T extends ContextToolName>(toolName: T, params: ContextToolParams<T>) => Promise<string>
|
||||
}
|
||||
|
||||
export const IToolService = createDecorator<IToolService>('ToolService');
|
||||
|
||||
export class ToolService implements IToolService {
|
||||
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
contextToolCallFns: AllContextToolCallFns
|
||||
|
||||
constructor(
|
||||
@IFileService fileService: IFileService,
|
||||
@IWorkspaceContextService workspaceContextService: IWorkspaceContextService,
|
||||
@ISearchService searchService: ISearchService,
|
||||
@IInstantiationService instantiationService: IInstantiationService,
|
||||
) {
|
||||
|
||||
|
||||
const queryBuilder = instantiationService.createInstance(QueryBuilder);
|
||||
|
||||
this.contextToolCallFns = {
|
||||
read_file: async ({ uri: uriStr }) => {
|
||||
const uri = validateURI(uriStr)
|
||||
const fileContents = await _VSReadFileRaw(fileService, uri)
|
||||
return fileContents ?? '(could not read file)'
|
||||
},
|
||||
list_dir: async ({ uri: uriStr }) => {
|
||||
const uri = validateURI(uriStr)
|
||||
const treeStr = await generateDirectoryTreeMd(fileService, uri)
|
||||
return treeStr
|
||||
},
|
||||
pathname_search: async ({ query: queryStr }) => {
|
||||
if (typeof queryStr !== 'string') return '(Error: query was not a string)'
|
||||
const query = queryBuilder.file(workspaceContextService.getWorkspace().folders.map(f => f.uri), { filePattern: queryStr, });
|
||||
|
||||
const data = await searchService.fileSearch(query, CancellationToken.None);
|
||||
const str = data.results.map(({ resource, results }) => resource.fsPath).join('\n')
|
||||
return str
|
||||
},
|
||||
search: async ({ query: queryStr }) => {
|
||||
if (typeof queryStr !== 'string') return '(Error: query was not a string)'
|
||||
const query = queryBuilder.text({ pattern: queryStr, }, workspaceContextService.getWorkspace().folders.map(f => f.uri));
|
||||
|
||||
const data = await searchService.textSearch(query, CancellationToken.None);
|
||||
const str = data.results.map(({ resource, results }) => resource.fsPath).join('\n')
|
||||
return str
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
callContextTool: IToolService['callContextTool'] = (toolName, params) => {
|
||||
return this.contextToolCallFns[toolName](params)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
registerSingleton(IToolService, ToolService, InstantiationType.Eager);
|
||||
|
||||
+131
-65
@@ -3,15 +3,15 @@
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Emitter, Event } from '../../../base/common/event.js';
|
||||
import { Disposable } from '../../../base/common/lifecycle.js';
|
||||
import { 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 { Emitter, Event } from '../../../../base/common/event.js';
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { deepClone } from '../../../../base/common/objects.js';
|
||||
import { IEncryptionService } from '../../../../platform/encryption/common/encryptionService.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 { IMetricsService } from './metricsService.js';
|
||||
import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, modelInfoOfDefaultNames, VoidModelInfo, GlobalSettings, GlobalSettingName, defaultGlobalSettings } from './voidSettingsTypes.js';
|
||||
import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, modelInfoOfDefaultModelNames, VoidModelInfo, GlobalSettings, GlobalSettingName, defaultGlobalSettings, displayInfoOfProviderName, defaultProviderSettings } from './voidSettingsTypes.js';
|
||||
|
||||
|
||||
const STORAGE_KEY = 'void.settingsServiceStorage'
|
||||
@@ -42,8 +42,8 @@ export type VoidSettingsState = {
|
||||
readonly _modelOptions: ModelOption[] // computed based on the two above items
|
||||
}
|
||||
|
||||
type RealVoidSettings = Exclude<keyof VoidSettingsState, '_modelOptions'>
|
||||
type EventProp<T extends RealVoidSettings = RealVoidSettings> = T extends 'globalSettings' ? [T, keyof VoidSettingsState[T]] : T | 'all'
|
||||
// type RealVoidSettings = Exclude<keyof VoidSettingsState, '_modelOptions'>
|
||||
// type EventProp<T extends RealVoidSettings = RealVoidSettings> = T extends 'globalSettings' ? [T, keyof VoidSettingsState[T]] : T | 'all'
|
||||
|
||||
|
||||
export interface IVoidSettingsService {
|
||||
@@ -51,7 +51,7 @@ export interface IVoidSettingsService {
|
||||
readonly state: VoidSettingsState; // in order to play nicely with react, you should immutably change state
|
||||
readonly waitForInitState: Promise<void>;
|
||||
|
||||
onDidChangeState: Event<EventProp>;
|
||||
onDidChangeState: Event<void>;
|
||||
|
||||
setSettingOfProvider: SetSettingOfProviderFn;
|
||||
setModelSelectionOfFeature: SetModelSelectionOfFeatureFn;
|
||||
@@ -64,26 +64,76 @@ export interface IVoidSettingsService {
|
||||
}
|
||||
|
||||
|
||||
let _computeModelOptions = (settingsOfProvider: SettingsOfProvider) => {
|
||||
let modelOptions: ModelOption[] = []
|
||||
|
||||
const _updatedValidatedState = (state: Omit<VoidSettingsState, '_modelOptions'>) => {
|
||||
|
||||
let newSettingsOfProvider = state.settingsOfProvider
|
||||
|
||||
// recompute _didFillInProviderSettings
|
||||
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({ name: `${modelName} (${providerName})`, selection: { providerName, modelName } })
|
||||
const settingsAtProvider = newSettingsOfProvider[providerName]
|
||||
|
||||
const didFillInProviderSettings = Object.keys(defaultProviderSettings[providerName]).every(key => !!settingsAtProvider[key as keyof typeof settingsAtProvider])
|
||||
|
||||
if (didFillInProviderSettings === settingsAtProvider._didFillInProviderSettings) continue
|
||||
|
||||
newSettingsOfProvider = {
|
||||
...newSettingsOfProvider,
|
||||
[providerName]: {
|
||||
...settingsAtProvider,
|
||||
_didFillInProviderSettings: didFillInProviderSettings,
|
||||
},
|
||||
}
|
||||
}
|
||||
return modelOptions
|
||||
|
||||
// update model options
|
||||
let newModelOptions: ModelOption[] = []
|
||||
for (const providerName of providerNames) {
|
||||
const providerTitle = displayInfoOfProviderName(providerName).title.toLowerCase() // looks better lowercase, best practice to not use raw providerName
|
||||
if (!newSettingsOfProvider[providerName]._didFillInProviderSettings) continue // if disabled, don't display model options
|
||||
for (const { modelName, isHidden } of newSettingsOfProvider[providerName].models) {
|
||||
if (isHidden) continue
|
||||
newModelOptions.push({ name: `${modelName} (${providerTitle})`, selection: { providerName, modelName } })
|
||||
}
|
||||
}
|
||||
|
||||
// now that model options are updated, make sure the selection is valid
|
||||
// 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)
|
||||
let newModelSelectionOfFeature = state.modelSelectionOfFeature
|
||||
for (const featureName of featureNames) {
|
||||
|
||||
const modelSelectionAtFeature = newModelSelectionOfFeature[featureName]
|
||||
const selnIdx = modelSelectionAtFeature === null ? -1 : newModelOptions.findIndex(m => modelSelectionsEqual(m.selection, modelSelectionAtFeature))
|
||||
|
||||
if (selnIdx !== -1) continue
|
||||
|
||||
newModelSelectionOfFeature = {
|
||||
...newModelSelectionOfFeature,
|
||||
[featureName]: newModelOptions.length === 0 ? null : newModelOptions[0].selection
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const newState = {
|
||||
...state,
|
||||
settingsOfProvider: newSettingsOfProvider,
|
||||
modelSelectionOfFeature: newModelSelectionOfFeature,
|
||||
_modelOptions: newModelOptions,
|
||||
} satisfies VoidSettingsState
|
||||
|
||||
return newState
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const defaultState = () => {
|
||||
const d: VoidSettingsState = {
|
||||
settingsOfProvider: deepClone(defaultSettingsOfProvider),
|
||||
modelSelectionOfFeature: { 'Ctrl+L': null, 'Ctrl+K': null, 'Autocomplete': null },
|
||||
modelSelectionOfFeature: { 'Ctrl+L': null, 'Ctrl+K': null, 'Autocomplete': null, 'FastApply': null },
|
||||
globalSettings: deepClone(defaultGlobalSettings),
|
||||
_modelOptions: _computeModelOptions(defaultSettingsOfProvider), // computed
|
||||
_modelOptions: [], // computed later
|
||||
}
|
||||
return d
|
||||
}
|
||||
@@ -93,8 +143,8 @@ export const IVoidSettingsService = createDecorator<IVoidSettingsService>('VoidS
|
||||
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
|
||||
private readonly _onDidChangeState = new Emitter<void>();
|
||||
readonly onDidChangeState: Event<void> = this._onDidChangeState.event; // this is primarily for use in react, so react can listen + update on state changes
|
||||
|
||||
state: VoidSettingsState;
|
||||
waitForInitState: Promise<void> // await this if you need a valid state initially
|
||||
@@ -115,13 +165,50 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
||||
this.waitForInitState = new Promise((res, rej) => resolver = res)
|
||||
|
||||
// read and update the actual state immediately
|
||||
this._readState().then(s => {
|
||||
this.state = s
|
||||
this._readState().then(readS => {
|
||||
|
||||
// the stored data structure might be outdated, so we need to update it here (can do a more general solution later when we need to)
|
||||
const newSettingsOfProvider = {
|
||||
// A HACK BECAUSE WE ADDED DEEPSEEK (did not exist before, comes before readS)
|
||||
...{ deepseek: defaultSettingsOfProvider.deepseek },
|
||||
|
||||
// A HACK BECAUSE WE ADDED MISTRAL (did not exist before, comes before readS)
|
||||
...{ mistral: defaultSettingsOfProvider.mistral },
|
||||
|
||||
...readS.settingsOfProvider,
|
||||
|
||||
// A HACK BECAUSE WE ADDED NEW GEMINI MODELS (existed before, comes after readS)
|
||||
gemini: {
|
||||
...readS.settingsOfProvider.gemini,
|
||||
models: [
|
||||
...readS.settingsOfProvider.gemini.models,
|
||||
...defaultSettingsOfProvider.gemini.models.filter(m => /* if cant find the model in readS (yes this is O(n^2), very small) */ !readS.settingsOfProvider.gemini.models.find(m2 => m2.modelName === m.modelName))
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
const newModelSelectionOfFeature = {
|
||||
// A HACK BECAUSE WE ADDED FastApply
|
||||
...{ 'FastApply': null },
|
||||
...readS.modelSelectionOfFeature,
|
||||
}
|
||||
|
||||
readS = {
|
||||
...readS,
|
||||
settingsOfProvider: newSettingsOfProvider,
|
||||
modelSelectionOfFeature: newModelSelectionOfFeature,
|
||||
}
|
||||
|
||||
this.state = _updatedValidatedState(readS)
|
||||
|
||||
resolver()
|
||||
this._onDidChangeState.fire('all')
|
||||
this._onDidChangeState.fire()
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
private async _readState(): Promise<VoidSettingsState> {
|
||||
const encryptedState = this._storageService.get(STORAGE_KEY, StorageScope.APPLICATION)
|
||||
|
||||
@@ -143,7 +230,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
||||
|
||||
const newModelSelectionOfFeature = this.state.modelSelectionOfFeature
|
||||
|
||||
const newSettingsOfProvider = {
|
||||
const newSettingsOfProvider: SettingsOfProvider = {
|
||||
...this.state.settingsOfProvider,
|
||||
[providerName]: {
|
||||
...this.state.settingsOfProvider[providerName],
|
||||
@@ -153,38 +240,17 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
||||
|
||||
const newGlobalSettings = this.state.globalSettings
|
||||
|
||||
// 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 = {
|
||||
const newState = {
|
||||
modelSelectionOfFeature: newModelSelectionOfFeature,
|
||||
settingsOfProvider: newSettingsOfProvider,
|
||||
globalSettings: newGlobalSettings,
|
||||
_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.selection, currentSelection))
|
||||
|
||||
if (selnIdx === -1) {
|
||||
if (newModelsList.length !== 0)
|
||||
this.setModelSelectionOfFeature(featureName, newModelsList[0].selection, { doNotApplyEffects: true })
|
||||
else
|
||||
this.setModelSelectionOfFeature(featureName, null, { doNotApplyEffects: true })
|
||||
}
|
||||
}
|
||||
}
|
||||
this.state = _updatedValidatedState(newState)
|
||||
|
||||
await this._storeState()
|
||||
this._onDidChangeState.fire('settingsOfProvider')
|
||||
this._onDidChangeState.fire()
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -198,7 +264,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
||||
}
|
||||
this.state = newState
|
||||
await this._storeState()
|
||||
this._onDidChangeState.fire(['globalSettings', settingName])
|
||||
this._onDidChangeState.fire()
|
||||
|
||||
}
|
||||
|
||||
@@ -218,7 +284,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
||||
return
|
||||
|
||||
await this._storeState()
|
||||
this._onDidChangeState.fire('modelSelectionOfFeature')
|
||||
this._onDidChangeState.fire()
|
||||
}
|
||||
|
||||
|
||||
@@ -227,23 +293,23 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
||||
|
||||
const { models } = this.state.settingsOfProvider[providerName]
|
||||
|
||||
const old_names = models.map(m => m.modelName)
|
||||
const oldModelNames = models.map(m => m.modelName)
|
||||
|
||||
const newDefaultModels = modelInfoOfDefaultNames(newDefaultModelNames, { isAutodetected: true, existingModels: models })
|
||||
const newModels = [
|
||||
...newDefaultModels,
|
||||
...models.filter(m => !m.isDefault), // keep any non-default models
|
||||
const newDefaultModelInfo = modelInfoOfDefaultModelNames(newDefaultModelNames, { isAutodetected: true, existingModels: models })
|
||||
const newModelInfo = [
|
||||
...newDefaultModelInfo, // swap out all the default models for the new default models
|
||||
...models.filter(m => !m.isDefault), // keep any non-defaul (custom) models
|
||||
]
|
||||
|
||||
|
||||
this.setSettingOfProvider(providerName, 'models', newModels)
|
||||
this.setSettingOfProvider(providerName, 'models', newModelInfo)
|
||||
|
||||
// if the models changed, log it
|
||||
const new_names = newModels.map(m => m.modelName)
|
||||
if (!(old_names.length === new_names.length
|
||||
&& old_names.every((_, i) => old_names[i] === new_names[i])
|
||||
)) {
|
||||
this._metricsService.capture('Autodetect Models', { providerName, newModels, ...logging })
|
||||
const new_names = newModelInfo.map(m => m.modelName)
|
||||
if (!(oldModelNames.length === new_names.length
|
||||
&& oldModelNames.every((_, i) => oldModelNames[i] === new_names[i]))
|
||||
) {
|
||||
this._metricsService.capture('Autodetect Models', { providerName, newModels: newModelInfo, ...logging })
|
||||
}
|
||||
}
|
||||
toggleModelHidden(providerName: ProviderName, modelName: string) {
|
||||
+250
-58
@@ -4,28 +4,28 @@
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
|
||||
import { VoidSettingsState } from './voidSettingsService.js'
|
||||
|
||||
|
||||
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
|
||||
isHidden: boolean, // whether or not the user is hiding it (switched off)
|
||||
isAutodetected?: boolean, // whether the model was autodetected by polling
|
||||
}
|
||||
|
||||
// creates `modelInfo` from `modelNames`
|
||||
export const modelInfoOfDefaultNames = (modelNames: string[], options?: { isAutodetected: true, existingModels: VoidModelInfo[] }): VoidModelInfo[] => {
|
||||
export const modelInfoOfDefaultModelNames = (defaultModelNames: string[], options?: { isAutodetected: true, existingModels: VoidModelInfo[] }): VoidModelInfo[] => {
|
||||
|
||||
const { isAutodetected, existingModels } = options ?? {}
|
||||
|
||||
if (!existingModels) { // default settings
|
||||
|
||||
return modelNames.map((modelName, i) => ({
|
||||
return defaultModelNames.map((modelName, i) => ({
|
||||
modelName,
|
||||
isDefault: true,
|
||||
isAutodetected: isAutodetected,
|
||||
isHidden: modelNames.length >= 10 // hide all models if there are a ton of them, and make user enable them individually
|
||||
isHidden: defaultModelNames.length >= 10 // hide all models if there are a ton of them, and make user enable them individually
|
||||
}))
|
||||
|
||||
} else { // settings if there are existing models (keep existing `isHidden` property)
|
||||
@@ -35,7 +35,7 @@ export const modelInfoOfDefaultNames = (modelNames: string[], options?: { isAuto
|
||||
existingModelsMap[existingModel.modelName] = existingModel
|
||||
}
|
||||
|
||||
return modelNames.map((modelName, i) => ({
|
||||
return defaultModelNames.map((modelName, i) => ({
|
||||
modelName,
|
||||
isDefault: true,
|
||||
isAutodetected: isAutodetected,
|
||||
@@ -47,7 +47,7 @@ export const modelInfoOfDefaultNames = (modelNames: string[], options?: { isAuto
|
||||
}
|
||||
|
||||
// https://docs.anthropic.com/en/docs/about-claude/models
|
||||
export const defaultAnthropicModels = modelInfoOfDefaultNames([
|
||||
export const defaultAnthropicModels = modelInfoOfDefaultModelNames([
|
||||
'claude-3-5-sonnet-20241022',
|
||||
'claude-3-5-haiku-20241022',
|
||||
'claude-3-opus-20240229',
|
||||
@@ -57,9 +57,10 @@ export const defaultAnthropicModels = modelInfoOfDefaultNames([
|
||||
|
||||
|
||||
// https://platform.openai.com/docs/models/gp
|
||||
export const defaultOpenAIModels = modelInfoOfDefaultNames([
|
||||
'o1-preview',
|
||||
export const defaultOpenAIModels = modelInfoOfDefaultModelNames([
|
||||
'o1',
|
||||
'o1-mini',
|
||||
'o3-mini',
|
||||
'gpt-4o',
|
||||
'gpt-4o-mini',
|
||||
// 'gpt-4o-2024-05-13',
|
||||
@@ -77,24 +78,42 @@ export const defaultOpenAIModels = modelInfoOfDefaultNames([
|
||||
// 'gpt-3.5-turbo-1106',
|
||||
])
|
||||
|
||||
// https://platform.openai.com/docs/models/gp
|
||||
export const defaultDeepseekModels = modelInfoOfDefaultModelNames([
|
||||
'deepseek-chat',
|
||||
'deepseek-reasoner',
|
||||
])
|
||||
|
||||
|
||||
// https://console.groq.com/docs/models
|
||||
export const defaultGroqModels = modelInfoOfDefaultNames([
|
||||
"mixtral-8x7b-32768",
|
||||
"llama2-70b-4096",
|
||||
"gemma-7b-it"
|
||||
export const defaultGroqModels = modelInfoOfDefaultModelNames([
|
||||
"llama3-70b-8192",
|
||||
"llama-3.3-70b-versatile",
|
||||
"llama-3.1-8b-instant",
|
||||
"gemma2-9b-it",
|
||||
"mixtral-8x7b-32768"
|
||||
])
|
||||
|
||||
|
||||
export const defaultGeminiModels = modelInfoOfDefaultNames([
|
||||
export const defaultGeminiModels = modelInfoOfDefaultModelNames([
|
||||
'gemini-1.5-flash',
|
||||
'gemini-1.5-pro',
|
||||
'gemini-1.5-flash-8b',
|
||||
'gemini-1.0-pro'
|
||||
'gemini-2.0-flash-exp',
|
||||
'gemini-2.0-flash-thinking-exp-1219',
|
||||
'learnlm-1.5-pro-experimental'
|
||||
])
|
||||
|
||||
|
||||
export const defaultMistralModels = modelInfoOfDefaultModelNames([
|
||||
"codestral-latest",
|
||||
"open-codestral-mamba",
|
||||
"open-mistral-nemo",
|
||||
"mistral-large-latest",
|
||||
"pixtral-large-latest",
|
||||
"ministral-3b-latest",
|
||||
"ministral-8b-latest",
|
||||
"mistral-small-latest",
|
||||
])
|
||||
|
||||
// export const parseMaxTokensStr = (maxTokensStr: string) => {
|
||||
// // parse the string but only if the full string is a valid number, eg parseInt('100abc') should return NaN
|
||||
@@ -130,6 +149,9 @@ export const defaultProviderSettings = {
|
||||
openAI: {
|
||||
apiKey: '',
|
||||
},
|
||||
deepseek: {
|
||||
apiKey: '',
|
||||
},
|
||||
ollama: {
|
||||
endpoint: 'http://127.0.0.1:11434',
|
||||
},
|
||||
@@ -144,6 +166,9 @@ export const defaultProviderSettings = {
|
||||
apiKey: '',
|
||||
},
|
||||
groq: {
|
||||
apiKey: '',
|
||||
},
|
||||
mistral: {
|
||||
apiKey: ''
|
||||
}
|
||||
} as const
|
||||
@@ -163,20 +188,22 @@ export const customSettingNamesOfProvider = (providerName: ProviderName) => {
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
type CommonProviderSettings = {
|
||||
_enabled: boolean | undefined, // undefined initially, computed when user types in all fields
|
||||
_didFillInProviderSettings: boolean | undefined, // undefined initially, computed when user types in all fields
|
||||
models: VoidModelInfo[],
|
||||
}
|
||||
|
||||
export type SettingsForProvider<providerName extends ProviderName> = CustomProviderSettings<providerName> & CommonProviderSettings
|
||||
export type SettingsAtProvider<providerName extends ProviderName> = CustomProviderSettings<providerName> & CommonProviderSettings
|
||||
|
||||
// part of state
|
||||
export type SettingsOfProvider = {
|
||||
[providerName in ProviderName]: SettingsForProvider<providerName>
|
||||
[providerName in ProviderName]: SettingsAtProvider<providerName>
|
||||
}
|
||||
|
||||
|
||||
export type SettingName = keyof SettingsForProvider<ProviderName>
|
||||
export type SettingName = keyof SettingsAtProvider<ProviderName>
|
||||
|
||||
|
||||
|
||||
@@ -198,6 +225,11 @@ export const displayInfoOfProviderName = (providerName: ProviderName): DisplayIn
|
||||
title: 'OpenAI',
|
||||
}
|
||||
}
|
||||
else if (providerName === 'deepseek') {
|
||||
return {
|
||||
title: 'DeepSeek.com API',
|
||||
}
|
||||
}
|
||||
else if (providerName === 'openRouter') {
|
||||
return {
|
||||
title: 'OpenRouter',
|
||||
@@ -216,12 +248,17 @@ export const displayInfoOfProviderName = (providerName: ProviderName): DisplayIn
|
||||
}
|
||||
else if (providerName === 'gemini') {
|
||||
return {
|
||||
title: 'Gemini',
|
||||
title: 'Gemini API',
|
||||
}
|
||||
}
|
||||
else if (providerName === 'groq') {
|
||||
return {
|
||||
title: 'Groq',
|
||||
title: 'Groq.com API',
|
||||
}
|
||||
}
|
||||
else if (providerName === 'mistral') {
|
||||
return {
|
||||
title: 'Mistral API',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,21 +274,28 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
|
||||
if (settingName === 'apiKey') {
|
||||
return {
|
||||
title: 'API Key',
|
||||
|
||||
// **Please follow this convention**:
|
||||
// The word "key..." here is a placeholder for the hash. For example, sk-ant-key... means the key will look like sk-ant-abcdefg123...
|
||||
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)',
|
||||
providerName === 'deepseek' ? 'sk-key...' :
|
||||
providerName === 'openRouter' ? 'sk-or-key...' : // sk-or-v1-key
|
||||
providerName === 'gemini' ? 'key...' :
|
||||
providerName === 'groq' ? 'gsk_key...' :
|
||||
providerName === 'mistral' ? 'key...' :
|
||||
providerName === 'openAICompatible' ? 'sk-key...' :
|
||||
'',
|
||||
|
||||
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,
|
||||
providerName === 'deepseek' ? 'Get your [API Key here](https://platform.deepseek.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 === 'mistral' ? 'Get your [API Key here](https://console.mistral.ai/api-keys/).' :
|
||||
providerName === 'openAICompatible' ? undefined :
|
||||
'',
|
||||
}
|
||||
}
|
||||
else if (settingName === 'endpoint') {
|
||||
@@ -268,7 +312,7 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
|
||||
undefined,
|
||||
}
|
||||
}
|
||||
else if (settingName === '_enabled') {
|
||||
else if (settingName === '_didFillInProviderSettings') {
|
||||
return {
|
||||
title: '(never)',
|
||||
placeholder: '(never)',
|
||||
@@ -293,6 +337,8 @@ const defaultCustomSettings: Record<CustomSettingName, undefined> = {
|
||||
endpoint: undefined,
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const voidInitModelOptions = {
|
||||
anthropic: {
|
||||
models: defaultAnthropicModels,
|
||||
@@ -300,6 +346,9 @@ export const voidInitModelOptions = {
|
||||
openAI: {
|
||||
models: defaultOpenAIModels,
|
||||
},
|
||||
deepseek: {
|
||||
models: defaultDeepseekModels,
|
||||
},
|
||||
ollama: {
|
||||
models: [],
|
||||
},
|
||||
@@ -315,52 +364,67 @@ export const voidInitModelOptions = {
|
||||
groq: {
|
||||
models: defaultGroqModels,
|
||||
},
|
||||
mistral: {
|
||||
models: defaultMistralModels,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// used when waiting and for a type reference
|
||||
export const defaultSettingsOfProvider: SettingsOfProvider = {
|
||||
anthropic: {
|
||||
_enabled: undefined,
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.anthropic,
|
||||
...voidInitModelOptions.anthropic,
|
||||
_didFillInProviderSettings: undefined,
|
||||
},
|
||||
openAI: {
|
||||
_enabled: undefined,
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.openAI,
|
||||
...voidInitModelOptions.openAI,
|
||||
_didFillInProviderSettings: undefined,
|
||||
},
|
||||
deepseek: {
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.deepseek,
|
||||
...voidInitModelOptions.deepseek,
|
||||
_didFillInProviderSettings: undefined,
|
||||
},
|
||||
gemini: {
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.gemini,
|
||||
...voidInitModelOptions.gemini,
|
||||
_enabled: undefined,
|
||||
_didFillInProviderSettings: undefined,
|
||||
},
|
||||
groq: {
|
||||
mistral: {
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.mistral,
|
||||
...voidInitModelOptions.mistral,
|
||||
_didFillInProviderSettings: undefined,
|
||||
},
|
||||
groq: { // aggregator
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.groq,
|
||||
...voidInitModelOptions.groq,
|
||||
_enabled: undefined,
|
||||
_didFillInProviderSettings: undefined,
|
||||
},
|
||||
ollama: {
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.ollama,
|
||||
...voidInitModelOptions.ollama,
|
||||
_enabled: undefined,
|
||||
},
|
||||
openRouter: {
|
||||
openRouter: { // aggregator
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.openRouter,
|
||||
...voidInitModelOptions.openRouter,
|
||||
_enabled: undefined,
|
||||
_didFillInProviderSettings: undefined,
|
||||
},
|
||||
openAICompatible: {
|
||||
openAICompatible: { // aggregator
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.openAICompatible,
|
||||
...voidInitModelOptions.openAICompatible,
|
||||
_enabled: undefined,
|
||||
_didFillInProviderSettings: undefined,
|
||||
},
|
||||
ollama: { // aggregator
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.ollama,
|
||||
...voidInitModelOptions.ollama,
|
||||
_didFillInProviderSettings: undefined,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -372,18 +436,22 @@ export const modelSelectionsEqual = (m1: ModelSelection, m2: ModelSelection) =>
|
||||
}
|
||||
|
||||
// this is a state
|
||||
export type ModelSelectionOfFeature = {
|
||||
'Ctrl+L': ModelSelection | null,
|
||||
'Ctrl+K': ModelSelection | null,
|
||||
'Autocomplete': ModelSelection | null,
|
||||
}
|
||||
export const featureNames = ['Ctrl+L', 'Ctrl+K', 'Autocomplete', 'FastApply'] as const
|
||||
export type ModelSelectionOfFeature = Record<(typeof featureNames)[number], ModelSelection | null>
|
||||
export type FeatureName = keyof ModelSelectionOfFeature
|
||||
export const featureNames = ['Ctrl+L', 'Ctrl+K', 'Autocomplete'] as const
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export const displayInfoOfFeatureName = (featureName: FeatureName) => {
|
||||
if (featureName === 'Autocomplete')
|
||||
return 'Autocomplete'
|
||||
else if (featureName === 'Ctrl+K')
|
||||
return 'Quick-Edit'
|
||||
else if (featureName === 'Ctrl+L')
|
||||
return 'Chat'
|
||||
else if (featureName === 'FastApply')
|
||||
return 'Apply'
|
||||
else
|
||||
throw new Error(`Feature Name ${featureName} not allowed`)
|
||||
}
|
||||
|
||||
|
||||
// the models of these can be refreshed (in theory all can, but not all should)
|
||||
@@ -395,6 +463,45 @@ export type RefreshableProviderName = typeof refreshableProviderNames[number]
|
||||
|
||||
|
||||
|
||||
// use this in isFeatuerNameDissbled
|
||||
export const isProviderNameDisabled = (providerName: ProviderName, settingsState: VoidSettingsState) => {
|
||||
|
||||
const settingsAtProvider = settingsState.settingsOfProvider[providerName]
|
||||
const isAutodetected = (refreshableProviderNames as string[]).includes(providerName)
|
||||
|
||||
const isDisabled = settingsAtProvider.models.length === 0
|
||||
if (isDisabled) {
|
||||
return isAutodetected ? 'providerNotAutoDetected' : (!settingsAtProvider._didFillInProviderSettings ? 'notFilledIn' : 'addModel')
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export const isFeatureNameDisabled = (featureName: FeatureName, settingsState: VoidSettingsState) => {
|
||||
// if has a selected provider, check if it's enabled
|
||||
const selectedProvider = settingsState.modelSelectionOfFeature[featureName]
|
||||
|
||||
if (selectedProvider) {
|
||||
const { providerName } = selectedProvider
|
||||
return isProviderNameDisabled(providerName, settingsState)
|
||||
}
|
||||
|
||||
// if there are any models they can turn on, tell them that
|
||||
const canTurnOnAModel = !!providerNames.find(providerName => settingsState.settingsOfProvider[providerName].models.filter(m => m.isHidden).length !== 0)
|
||||
if (canTurnOnAModel) return 'needToEnableModel'
|
||||
|
||||
// if there are any providers filled in, then they just need to add a model
|
||||
const anyFilledIn = !!providerNames.find(providerName => settingsState.settingsOfProvider[providerName]._didFillInProviderSettings)
|
||||
if (anyFilledIn) return 'addModel'
|
||||
|
||||
return 'addProvider'
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export type GlobalSettings = {
|
||||
@@ -410,3 +517,88 @@ export type GlobalSettingName = keyof GlobalSettings
|
||||
export const globalSettingNames = Object.keys(defaultGlobalSettings) as GlobalSettingName[]
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export const recognizedModels = [
|
||||
|
||||
// chat
|
||||
'OpenAI 4o',
|
||||
'Anthropic Claude',
|
||||
'Llama 3.x',
|
||||
'Deepseek Chat', // deepseek coder v2 is now merged into chat (V3) https://api-docs.deepseek.com/updates#deepseek-coder--deepseek-chat-upgraded-to-deepseek-v25-model
|
||||
// 'xAI Grok',
|
||||
// 'Google Gemini, Gemma',
|
||||
// 'Microsoft Phi4',
|
||||
|
||||
|
||||
// coding (autocomplete)
|
||||
'Alibaba Qwen2.5 Coder Instruct', // we recommend this over Qwen2.5
|
||||
'Mistral Codestral',
|
||||
|
||||
// thinking
|
||||
'OpenAI o1, o3',
|
||||
'Deepseek R1',
|
||||
|
||||
// general
|
||||
'<General>'
|
||||
// 'Mixtral 8x7b'
|
||||
// 'Qwen2.5',
|
||||
|
||||
] as const
|
||||
|
||||
|
||||
|
||||
|
||||
type RecognizedModel = (typeof recognizedModels)[number]
|
||||
|
||||
|
||||
// const modelCapabilities: { [recognizedModel in RecognizedModel]: ({ }) => string } = {
|
||||
// 'OpenAI 4o': {
|
||||
// template: ({ prefix, suffix, }: { prefix: string; suffix: string; }) => `\
|
||||
// `
|
||||
// }
|
||||
// }
|
||||
|
||||
export function getRecognizedModel(modelName: string): RecognizedModel {
|
||||
const lower = modelName.toLowerCase();
|
||||
|
||||
if (lower.includes('gpt-4o')) {
|
||||
return 'OpenAI 4o';
|
||||
}
|
||||
if (lower.includes('claude')) {
|
||||
return 'Anthropic Claude';
|
||||
}
|
||||
if (lower.includes('llama')) {
|
||||
return 'Llama 3.x';
|
||||
}
|
||||
if (lower.includes('qwen2.5-coder')) {
|
||||
return 'Alibaba Qwen2.5 Coder Instruct';
|
||||
}
|
||||
if (lower.includes('mistral')) {
|
||||
return 'Mistral Codestral';
|
||||
}
|
||||
// Check for "o1" or "o3"
|
||||
if (/\bo1\b/.test(lower) || /\bo3\b/.test(lower)) {
|
||||
return 'OpenAI o1, o3';
|
||||
}
|
||||
if (lower.includes('deepseek-r1') || lower.includes('deepseek-reasoner')) {
|
||||
return 'Deepseek R1';
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Fallback:
|
||||
return '<General>';
|
||||
}
|
||||
+4
-5
@@ -3,10 +3,10 @@
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { createDecorator } from '../../instantiation/common/instantiation.js';
|
||||
import { ProxyChannel } from '../../../base/parts/ipc/common/ipc.js';
|
||||
import { IMainProcessService } from '../../ipc/common/mainProcessService.js';
|
||||
import { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js';
|
||||
import { ProxyChannel } from '../../../../base/parts/ipc/common/ipc.js';
|
||||
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { IMainProcessService } from '../../../../platform/ipc/common/mainProcessService.js';
|
||||
|
||||
|
||||
|
||||
@@ -33,7 +33,6 @@ export class VoidUpdateService implements IVoidUpdateService {
|
||||
}
|
||||
|
||||
|
||||
|
||||
// anything transmitted over a channel must be async even if it looks like it doesn't have to be
|
||||
check: IVoidUpdateService['check'] = async () => {
|
||||
const res = await this.voidUpdateService.check()
|
||||
+24
-19
@@ -4,15 +4,31 @@
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import Anthropic from '@anthropic-ai/sdk';
|
||||
import { _InternalSendLLMMessageFnType } from '../../common/llmMessageTypes.js';
|
||||
import { _InternalSendLLMChatMessageFnType } from '../../common/llmMessageTypes.js';
|
||||
import { anthropicMaxPossibleTokens } from '../../common/voidSettingsTypes.js';
|
||||
import { InternalToolInfo } from '../../common/toolsService.js';
|
||||
|
||||
// Anthropic
|
||||
type LLMMessageAnthropic = {
|
||||
role: 'user' | 'assistant';
|
||||
content: string;
|
||||
|
||||
|
||||
|
||||
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
|
||||
}
|
||||
export const sendAnthropicMsg: _InternalSendLLMMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export const sendAnthropicChat: _InternalSendLLMChatMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => {
|
||||
|
||||
const thisConfig = settingsOfProvider.anthropic
|
||||
|
||||
@@ -24,20 +40,9 @@ export const sendAnthropicMsg: _InternalSendLLMMessageFnType = ({ messages, onTe
|
||||
|
||||
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: anthropicMessages,
|
||||
// system: systemMessage,
|
||||
messages: messages,
|
||||
model: modelName,
|
||||
max_tokens: maxTokens,
|
||||
});
|
||||
+8
-18
@@ -3,11 +3,11 @@
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Content, GoogleGenerativeAI, GoogleGenerativeAIFetchError } from '@google/generative-ai';
|
||||
import { _InternalSendLLMMessageFnType } from '../../common/llmMessageTypes.js';
|
||||
import { Content, GoogleGenerativeAI } from '@google/generative-ai';
|
||||
import { _InternalSendLLMChatMessageFnType } from '../../common/llmMessageTypes.js';
|
||||
|
||||
// Gemini
|
||||
export const sendGeminiMsg: _InternalSendLLMMessageFnType = async ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => {
|
||||
export const sendGeminiChat: _InternalSendLLMChatMessageFnType = async ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => {
|
||||
|
||||
let fullText = ''
|
||||
|
||||
@@ -16,22 +16,17 @@ export const sendGeminiMsg: _InternalSendLLMMessageFnType = async ({ messages, o
|
||||
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, })
|
||||
model.generateContentStream({
|
||||
// systemInstruction: systemMessage,
|
||||
contents: geminiMessages,
|
||||
})
|
||||
.then(async response => {
|
||||
_setAborter(() => response.stream.return(fullText))
|
||||
|
||||
@@ -43,11 +38,6 @@ export const sendGeminiMsg: _InternalSendLLMMessageFnType = async ({ messages, o
|
||||
onFinalMessage({ fullText });
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error instanceof GoogleGenerativeAIFetchError && error.status === 400) {
|
||||
onError({ message: 'Invalid API key.', fullError: null });
|
||||
}
|
||||
else {
|
||||
onError({ message: error + '', fullError: error });
|
||||
}
|
||||
onError({ message: error + '', fullError: error })
|
||||
})
|
||||
}
|
||||
+4
-8
@@ -4,10 +4,10 @@
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import Groq from 'groq-sdk';
|
||||
import { _InternalSendLLMMessageFnType } from '../../common/llmMessageTypes.js';
|
||||
import { _InternalSendLLMChatMessageFnType } from '../../common/llmMessageTypes.js';
|
||||
|
||||
// Groq
|
||||
export const sendGroqMsg: _InternalSendLLMMessageFnType = async ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => {
|
||||
export const sendGroqChat: _InternalSendLLMChatMessageFnType = async ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => {
|
||||
let fullText = '';
|
||||
|
||||
const thisConfig = settingsOfProvider.groq
|
||||
@@ -22,18 +22,14 @@ export const sendGroqMsg: _InternalSendLLMMessageFnType = async ({ messages, onT
|
||||
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 || '';
|
||||
if (newText) {
|
||||
fullText += newText;
|
||||
onText({ newText, fullText });
|
||||
}
|
||||
fullText += newText;
|
||||
onText({ newText, fullText });
|
||||
}
|
||||
|
||||
onFinalMessage({ fullText });
|
||||
@@ -0,0 +1,44 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Mistral } from '@mistralai/mistralai';
|
||||
import { _InternalSendLLMChatMessageFnType } from '../../common/llmMessageTypes.js';
|
||||
|
||||
// Mistral
|
||||
export const sendMistralChat: _InternalSendLLMChatMessageFnType = async ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => {
|
||||
let fullText = '';
|
||||
|
||||
const thisConfig = settingsOfProvider.mistral;
|
||||
|
||||
const mistral = new Mistral({
|
||||
apiKey: thisConfig.apiKey,
|
||||
})
|
||||
|
||||
await mistral.chat
|
||||
.stream({
|
||||
messages: messages,
|
||||
model: modelName,
|
||||
stream: true,
|
||||
})
|
||||
.then(async response => {
|
||||
// Mistral has a really nonstandard API - no interrupt and weird stream types
|
||||
_setAborter(() => { console.log('Mistral does not support interrupts! Further messages will just be ignored.') });
|
||||
// when receive text
|
||||
for await (const chunk of response) {
|
||||
const c = chunk.data.choices[0].delta.content || ''
|
||||
const newText = (
|
||||
typeof c === 'string' ? c
|
||||
: c?.map(c => c.type === 'text' ? c.text : c.type).join('\n')
|
||||
)
|
||||
fullText += newText;
|
||||
onText({ newText, fullText });
|
||||
}
|
||||
|
||||
onFinalMessage({ fullText });
|
||||
})
|
||||
.catch(error => {
|
||||
onError({ message: error + '', fullError: error });
|
||||
})
|
||||
}
|
||||
+41
-10
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Ollama } from 'ollama';
|
||||
import { _InternalModelListFnType, _InternalSendLLMMessageFnType, OllamaModelResponse } from '../../common/llmMessageTypes.js';
|
||||
import { _InternalModelListFnType, _InternalSendLLMFIMMessageFnType, _InternalSendLLMChatMessageFnType, OllamaModelResponse } from '../../common/llmMessageTypes.js';
|
||||
import { defaultProviderSettings } from '../../common/voidSettingsTypes.js';
|
||||
|
||||
export const ollamaList: _InternalModelListFnType<OllamaModelResponse> = async ({ onSuccess: onSuccess_, onError: onError_, settingsOfProvider }) => {
|
||||
@@ -38,8 +38,47 @@ 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 sendOllamaMsg: _InternalSendLLMMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => {
|
||||
export const sendOllamaChat: _InternalSendLLMChatMessageFnType = ({ 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
|
||||
@@ -68,14 +107,6 @@ export const sendOllamaMsg: _InternalSendLLMMessageFnType = ({ messages, onText,
|
||||
})
|
||||
// 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,159 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import OpenAI from 'openai';
|
||||
import { _InternalModelListFnType, _InternalSendLLMFIMMessageFnType, _InternalSendLLMChatMessageFnType } from '../../common/llmMessageTypes.js';
|
||||
import { Model } from 'openai/resources/models.js';
|
||||
import { InternalToolInfo } from '../../common/toolsService.js';
|
||||
// import { parseMaxTokensStr } from './util.js';
|
||||
|
||||
|
||||
// developer command - https://cdn.openai.com/spec/model-spec-2024-05-08.html#follow-the-chain-of-command
|
||||
// prompting - https://platform.openai.com/docs/guides/reasoning#advice-on-prompting
|
||||
|
||||
|
||||
export const toOpenAITool = (toolName: string, toolInfo: InternalToolInfo) => {
|
||||
const { description, params, required } = toolInfo
|
||||
return {
|
||||
type: 'function',
|
||||
function: {
|
||||
name: toolName,
|
||||
description: description,
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: params,
|
||||
required: required,
|
||||
}
|
||||
}
|
||||
} satisfies OpenAI.Chat.Completions.ChatCompletionTool
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
type NewParams = Pick<Parameters<_InternalSendLLMChatMessageFnType>[0] & Parameters<_InternalSendLLMFIMMessageFnType>[0], 'settingsOfProvider' | 'providerName'>
|
||||
const newOpenAI = ({ settingsOfProvider, providerName }: NewParams) => {
|
||||
|
||||
if (providerName === 'openAI') {
|
||||
const thisConfig = settingsOfProvider.openAI
|
||||
return new OpenAI({ apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true });
|
||||
}
|
||||
else if (providerName === 'openRouter') {
|
||||
const thisConfig = settingsOfProvider.openRouter
|
||||
return 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.
|
||||
},
|
||||
})
|
||||
}
|
||||
else if (providerName === 'deepseek') {
|
||||
const thisConfig = settingsOfProvider.deepseek
|
||||
return new OpenAI({
|
||||
baseURL: 'https://api.deepseek.com/v1', apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true,
|
||||
})
|
||||
|
||||
}
|
||||
else if (providerName === 'openAICompatible') {
|
||||
const thisConfig = settingsOfProvider.openAICompatible
|
||||
return new OpenAI({
|
||||
baseURL: thisConfig.endpoint, apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true
|
||||
})
|
||||
}
|
||||
else {
|
||||
console.error(`sendOpenAIMsg: invalid providerName: ${providerName}`)
|
||||
throw new Error(`providerName was invalid: ${providerName}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// might not currently be used in the code
|
||||
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 openai = newOpenAI({ providerName: 'openAICompatible', settingsOfProvider })
|
||||
|
||||
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 + '' })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
export const sendOpenAIFIM: _InternalSendLLMFIMMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }) => {
|
||||
|
||||
|
||||
// openai.completions has a FIM parameter called `suffix`, but it's deprecated and only works for ~GPT 3 era models
|
||||
|
||||
onFinalMessage({ fullText: 'TODO' })
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// OpenAI, OpenRouter, OpenAICompatible
|
||||
export const sendOpenAIChat: _InternalSendLLMChatMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }) => {
|
||||
|
||||
let fullText = ''
|
||||
|
||||
const openai: OpenAI = newOpenAI({ providerName, settingsOfProvider })
|
||||
const options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = {
|
||||
model: modelName,
|
||||
messages: messages,
|
||||
stream: true,
|
||||
// tools: Object.keys(contextTools).map(name => toOpenAITool(name, contextTools[name as ContextToolName])),
|
||||
}
|
||||
|
||||
openai.chat.completions
|
||||
.create(options)
|
||||
.then(async response => {
|
||||
_setAborter(() => response.controller.abort())
|
||||
// when receive text
|
||||
for await (const chunk of response) {
|
||||
|
||||
let newText = ''
|
||||
newText += chunk.choices[0]?.delta?.tool_calls?.[0]?.function?.name ?? ''
|
||||
newText += chunk.choices[0]?.delta?.tool_calls?.[0]?.function?.arguments ?? ''
|
||||
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,178 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { SendLLMMessageParams, OnText, OnFinalMessage, OnError, LLMChatMessage, _InternalLLMChatMessage } from '../../common/llmMessageTypes.js';
|
||||
import { IMetricsService } from '../../common/metricsService.js';
|
||||
|
||||
import { sendAnthropicChat } from './anthropic.js';
|
||||
import { sendOllamaFIM, sendOllamaChat } from './ollama.js';
|
||||
import { sendOpenAIChat, sendOpenAIFIM } from './openai.js';
|
||||
import { sendGeminiChat } from './gemini.js';
|
||||
import { sendGroqChat } from './groq.js';
|
||||
import { sendMistralChat } from './mistral.js';
|
||||
import { displayInfoOfProviderName } from '../../common/voidSettingsTypes.js';
|
||||
|
||||
|
||||
const cleanChatMessages = (messages: LLMChatMessage[]): _InternalLLMChatMessage[] => {
|
||||
// trim message content (Anthropic and other providers give an error if there is trailing whitespace)
|
||||
messages = messages.map(m => ({ ...m, content: m.content.trim() }))
|
||||
|
||||
// find system messages and concatenate them
|
||||
const systemMessage = messages
|
||||
.filter(msg => msg.role === 'system')
|
||||
.map(msg => msg.content)
|
||||
.join('\n') || undefined;
|
||||
|
||||
// remove all system messages
|
||||
const noSystemMessages = messages
|
||||
.filter(msg => msg.role !== 'system') as _InternalLLMChatMessage[]
|
||||
|
||||
// add system mesasges to first message (should be a user message)
|
||||
if (systemMessage && (noSystemMessages.length !== 0)) {
|
||||
const newFirstMessage = {
|
||||
role: noSystemMessages[0].role,
|
||||
content: (''
|
||||
+ '<SYSTEM_MESSAGE>\n'
|
||||
+ systemMessage
|
||||
+ '\n'
|
||||
+ '</SYSTEM_MESSAGE>\n'
|
||||
+ noSystemMessages[0].content
|
||||
)
|
||||
}
|
||||
noSystemMessages.splice(0, 1) // delete first message
|
||||
noSystemMessages.unshift(newFirstMessage) // add new first message
|
||||
}
|
||||
|
||||
return noSystemMessages
|
||||
}
|
||||
|
||||
|
||||
export const sendLLMMessage = ({
|
||||
messagesType,
|
||||
aiInstructions,
|
||||
messages: messages_,
|
||||
onText: onText_,
|
||||
onFinalMessage: onFinalMessage_,
|
||||
onError: onError_,
|
||||
abortRef: abortRef_,
|
||||
logging: { loggingName },
|
||||
settingsOfProvider,
|
||||
providerName,
|
||||
modelName,
|
||||
}: SendLLMMessageParams,
|
||||
|
||||
metricsService: IMetricsService
|
||||
) => {
|
||||
|
||||
let messagesArr: _InternalLLMChatMessage[] = []
|
||||
if (messagesType === 'chatMessages') {
|
||||
messagesArr = cleanChatMessages([
|
||||
{ role: 'system', content: aiInstructions },
|
||||
...messages_
|
||||
])
|
||||
}
|
||||
|
||||
// only captures number of messages and message "shape", no actual code, instructions, prompts, etc
|
||||
const captureLLMEvent = (eventId: string, extras?: object) => {
|
||||
metricsService.capture(eventId, {
|
||||
providerName,
|
||||
modelName,
|
||||
...messagesType === 'chatMessages' ? {
|
||||
numMessages: messagesArr?.length,
|
||||
messagesShape: messagesArr?.map(msg => ({ role: msg.role, length: msg.content.length })),
|
||||
origNumMessages: messages_?.length,
|
||||
origMessagesShape: messages_?.map(msg => ({ role: msg.role, length: msg.content.length })),
|
||||
|
||||
} : messagesType === 'FIMMessage' ? {
|
||||
prefixLength: messages_.prefix.length,
|
||||
suffixLength: messages_.suffix.length,
|
||||
} : {},
|
||||
|
||||
...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
|
||||
captureLLMEvent(`${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.error('sendLLMMessage onError:', error)
|
||||
|
||||
// handle failed to fetch errors, which give 0 information by design
|
||||
if (error === 'TypeError: fetch failed')
|
||||
error = `Failed to fetch from ${displayInfoOfProviderName(providerName).title}. This likely means you specified the wrong endpoint in Void Settings, or your local model provider like Ollama is powered off.`
|
||||
|
||||
captureLLMEvent(`${loggingName} - Error`, { error })
|
||||
onError_({ message: error, fullError })
|
||||
}
|
||||
|
||||
const onAbort = () => {
|
||||
captureLLMEvent(`${loggingName} - Abort`, { messageLengthSoFar: _fullTextSoFar.length })
|
||||
try { _aborter?.() } // aborter sometimes automatically throws an error
|
||||
catch (e) { }
|
||||
_didAbort = true
|
||||
}
|
||||
abortRef_.current = onAbort
|
||||
|
||||
captureLLMEvent(`${loggingName} - Sending Message`, { messageLength: messagesArr[messagesArr.length - 1]?.content.length })
|
||||
|
||||
try {
|
||||
switch (providerName) {
|
||||
case 'anthropic':
|
||||
sendAnthropicChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
|
||||
break;
|
||||
case 'openAI':
|
||||
case 'openRouter':
|
||||
case 'deepseek':
|
||||
case 'openAICompatible':
|
||||
if (messagesType === 'FIMMessage') sendOpenAIFIM({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
|
||||
else /* */ sendOpenAIChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
|
||||
break;
|
||||
case 'ollama':
|
||||
if (messagesType === 'FIMMessage') sendOllamaFIM({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName })
|
||||
else /* */ sendOllamaChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName })
|
||||
break;
|
||||
case 'gemini':
|
||||
sendGeminiChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
|
||||
break;
|
||||
case 'groq':
|
||||
sendGroqChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
|
||||
break;
|
||||
case 'mistral':
|
||||
sendMistralChat({ messages: messagesArr, 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
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
+6
-6
@@ -6,9 +6,9 @@
|
||||
// registered in app.ts
|
||||
// code convention is to make a service responsible for this stuff, and not a channel, but having fewer files is simpler...
|
||||
|
||||
import { IServerChannel } from '../../../base/parts/ipc/common/ipc.js';
|
||||
import { Emitter, Event } from '../../../base/common/event.js';
|
||||
import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, MainLLMMessageParams, AbortRef, LLMMMessageParams, MainLLMMessageAbortParams, 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, MainSendLLMMessageParams, AbortRef, SendLLMMessageParams, 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
|
||||
// browser uses this to call (see this.channel.call() in llmMessageService.ts for all usages)
|
||||
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: MainLLMMessageParams) {
|
||||
private async _callSendLLMMessage(params: MainSendLLMMessageParams) {
|
||||
const { requestId } = params;
|
||||
|
||||
if (!(requestId in this._abortRefOfRequestId_llm))
|
||||
this._abortRefOfRequestId_llm[requestId] = { current: null }
|
||||
|
||||
const mainThreadParams: LLMMMessageParams = {
|
||||
const mainThreadParams: SendLLMMessageParams = {
|
||||
...params,
|
||||
onText: ({ newText, fullText }) => { this._onText_llm.fire({ requestId, newText, fullText }); },
|
||||
onFinalMessage: ({ fullText }) => { this._onFinalMessage_llm.fire({ requestId, fullText }); },
|
||||
@@ -0,0 +1,138 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* 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 { isLinux, isMacintosh, isWindows } from '../../../../base/common/platform.js';
|
||||
import { generateUuid } from '../../../../base/common/uuid.js';
|
||||
import { IEnvironmentMainService } from '../../../../platform/environment/electron-main/environmentMainService.js';
|
||||
import { IProductService } from '../../../../platform/product/common/productService.js';
|
||||
import { StorageTarget, StorageScope } from '../../../../platform/storage/common/storage.js';
|
||||
import { IApplicationStorageMainService } from '../../../../platform/storage/electron-main/storageMainService.js';
|
||||
|
||||
import { IMetricsService } from '../common/metricsService.js';
|
||||
import { PostHog } from 'posthog-node'
|
||||
|
||||
|
||||
const os = isWindows ? 'windows' : isMacintosh ? 'mac' : isLinux ? 'linux' : null
|
||||
const _getOSInfo = () => {
|
||||
try {
|
||||
const { platform, arch } = process // see platform.ts
|
||||
return { platform, arch }
|
||||
}
|
||||
catch (e) {
|
||||
return { osInfo: { platform: '??', arch: '??' } }
|
||||
}
|
||||
}
|
||||
const osInfo = _getOSInfo()
|
||||
|
||||
// we'd like to use devDeviceId on telemetryService, but that gets sanitized by the time it gets here as 'someValue.devDeviceId'
|
||||
|
||||
export class MetricsMainService extends Disposable implements IMetricsService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
private readonly client: PostHog
|
||||
|
||||
private _initProperties: object = {}
|
||||
|
||||
|
||||
// helper - looks like this is stored in a .vscdb file in ~/Library/Application Support/Void
|
||||
private _memoStorage(key: string, target: StorageTarget, setValIfNotExist?: string) {
|
||||
const currVal = this._appStorage.get(key, StorageScope.APPLICATION)
|
||||
if (currVal !== undefined) return currVal
|
||||
const newVal = setValIfNotExist ?? generateUuid()
|
||||
this._appStorage.store(key, newVal, StorageScope.APPLICATION, target)
|
||||
return newVal
|
||||
}
|
||||
|
||||
|
||||
// this is old, eventually we can just delete this since all the keys will have been transferred over
|
||||
// returns 'NULL' or the old key
|
||||
private get oldId() {
|
||||
// check new storage key first
|
||||
const newKey = 'void.app.oldMachineId'
|
||||
const newOldId = this._appStorage.get(newKey, StorageScope.APPLICATION)
|
||||
if (newOldId) return newOldId
|
||||
|
||||
// put old key into new key if didn't already
|
||||
const oldValue = this._appStorage.get('void.machineId', StorageScope.APPLICATION) ?? 'NULL' // the old way of getting the key
|
||||
this._appStorage.store(newKey, oldValue, StorageScope.APPLICATION, StorageTarget.MACHINE)
|
||||
return oldValue
|
||||
|
||||
// in a few weeks we can replace above with this
|
||||
// private get oldId() {
|
||||
// return this._memoStorage('void.app.oldMachineId', StorageTarget.MACHINE, 'NULL')
|
||||
// }
|
||||
}
|
||||
|
||||
|
||||
// the main id
|
||||
private get distinctId() {
|
||||
const oldId = this.oldId
|
||||
const setValIfNotExist = oldId === 'NULL' ? undefined : oldId
|
||||
return this._memoStorage('void.app.machineId', StorageTarget.MACHINE, setValIfNotExist)
|
||||
}
|
||||
|
||||
// just to see if there are ever multiple machineIDs per userID (instead of this, we should just track by the user's email)
|
||||
private get userId() {
|
||||
return this._memoStorage('void.app.userMachineId', StorageTarget.USER)
|
||||
}
|
||||
|
||||
constructor(
|
||||
@IProductService private readonly _productService: IProductService,
|
||||
@IEnvironmentMainService private readonly _envMainService: IEnvironmentMainService,
|
||||
@IApplicationStorageMainService private readonly _appStorage: IApplicationStorageMainService,
|
||||
) {
|
||||
super()
|
||||
this.client = new PostHog('phc_UanIdujHiLp55BkUTjB1AuBXcasVkdqRwgnwRlWESH2', {
|
||||
host: 'https://us.i.posthog.com',
|
||||
})
|
||||
|
||||
this.initialize() // async
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
// very important to await whenReady!
|
||||
await this._appStorage.whenReady
|
||||
|
||||
const { commit, version, quality } = this._productService
|
||||
|
||||
const isDevMode = !this._envMainService.isBuilt // found in abstractUpdateService.ts
|
||||
|
||||
// custom properties we identify
|
||||
this._initProperties = {
|
||||
commit,
|
||||
vscodeVersion: version,
|
||||
os,
|
||||
quality,
|
||||
distinctId: this.distinctId,
|
||||
distinctIdUser: this.userId,
|
||||
oldId: this.oldId,
|
||||
isDevMode,
|
||||
...osInfo,
|
||||
}
|
||||
|
||||
const identifyMessage = {
|
||||
distinctId: this.distinctId,
|
||||
properties: this._initProperties,
|
||||
}
|
||||
this.client.identify(identifyMessage)
|
||||
|
||||
console.log('Void posthog metrics info:', JSON.stringify(identifyMessage, null, 2))
|
||||
}
|
||||
|
||||
|
||||
capture: IMetricsService['capture'] = (event, params) => {
|
||||
const capture = { distinctId: this.distinctId, event, properties: params } as const
|
||||
// console.log('full capture:', capture)
|
||||
this.client.capture(capture)
|
||||
}
|
||||
|
||||
|
||||
async getDebuggingProperties() {
|
||||
return this._initProperties
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
|
||||
modelName -> {
|
||||
system_message_type: 'system' | 'developer' (openai) | null // if null, we will just do a string of system message
|
||||
supports_tools: boolean // we will just do a string of tool use if it doesn't support
|
||||
supports_autocomplete_FIM (suffix) // we will just do a description of FIM if it doens't support <|fim_hole|>
|
||||
|
||||
supports_streaming: boolean // (o1 does NOT) we will just dump the final result if doesn't support it
|
||||
max_tokens: number // required, DEFAULT is Infinity
|
||||
|
||||
}
|
||||
|
||||
*/
|
||||
+3
-4
@@ -3,10 +3,9 @@
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable } from '../../../base/common/lifecycle.js';
|
||||
import { IEnvironmentMainService } from '../../environment/electron-main/environmentMainService.js';
|
||||
|
||||
import { IProductService } from '../../product/common/productService.js';
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { IEnvironmentMainService } from '../../../../platform/environment/electron-main/environmentMainService.js';
|
||||
import { IProductService } from '../../../../platform/product/common/productService.js';
|
||||
|
||||
import { IVoidUpdateService } from '../common/voidUpdateService.js';
|
||||
|
||||
@@ -44,45 +44,77 @@ export enum ThemeSettings {
|
||||
}
|
||||
|
||||
export enum ThemeSettingDefaults {
|
||||
COLOR_THEME_DARK = 'Default Dark Modern',
|
||||
COLOR_THEME_DARK = 'Default Dark+', // Void changed this. this is the default theme
|
||||
COLOR_THEME_LIGHT = 'Default Light Modern',
|
||||
COLOR_THEME_HC_DARK = 'Default High Contrast',
|
||||
COLOR_THEME_HC_LIGHT = 'Default High Contrast Light',
|
||||
|
||||
COLOR_THEME_DARK_OLD = 'Default Dark+',
|
||||
COLOR_THEME_DARK_OLD = 'Default Dark Modern', // Void changed this
|
||||
COLOR_THEME_LIGHT_OLD = 'Default Light+',
|
||||
|
||||
FILE_ICON_THEME = 'vs-seti',
|
||||
PRODUCT_ICON_THEME = 'Default',
|
||||
}
|
||||
|
||||
export const COLOR_THEME_DARK_INITIAL_COLORS = {
|
||||
'activityBar.activeBorder': '#0078d4',
|
||||
'activityBar.background': '#181818',
|
||||
'activityBar.border': '#2b2b2b',
|
||||
'activityBar.foreground': '#d7d7d7',
|
||||
'activityBar.inactiveForeground': '#868686',
|
||||
'editorGroup.border': '#ffffff17',
|
||||
'editorGroupHeader.tabsBackground': '#181818',
|
||||
'editorGroupHeader.tabsBorder': '#2b2b2b',
|
||||
'statusBar.background': '#181818',
|
||||
'statusBar.border': '#2b2b2b',
|
||||
'statusBar.foreground': '#cccccc',
|
||||
'statusBar.noFolderBackground': '#1f1f1f',
|
||||
'tab.activeBackground': '#1f1f1f',
|
||||
'tab.activeBorder': '#1f1f1f',
|
||||
'tab.activeBorderTop': '#0078d4',
|
||||
// export const COLOR_THEME_DARK_INITIAL_COLORS = {
|
||||
// 'activityBar.activeBorder': '#0078d4',
|
||||
// 'activityBar.background': '#181818',
|
||||
// 'activityBar.border': '#2b2b2b',
|
||||
// 'activityBar.foreground': '#d7d7d7',
|
||||
// 'activityBar.inactiveForeground': '#868686',
|
||||
// 'editorGroup.border': '#ffffff17',
|
||||
// 'editorGroupHeader.tabsBackground': '#181818',
|
||||
// 'editorGroupHeader.tabsBorder': '#2b2b2b',
|
||||
// 'statusBar.background': '#181818',
|
||||
// 'statusBar.border': '#2b2b2b',
|
||||
// 'statusBar.foreground': '#cccccc',
|
||||
// 'statusBar.noFolderBackground': '#1f1f1f',
|
||||
// 'tab.activeBackground': '#1f1f1f',
|
||||
// 'tab.activeBorder': '#1f1f1f',
|
||||
// 'tab.activeBorderTop': '#0078d4',
|
||||
// 'tab.activeForeground': '#ffffff',
|
||||
// 'tab.border': '#2b2b2b',
|
||||
// 'textLink.foreground': '#4daafc',
|
||||
// 'titleBar.activeBackground': '#181818',
|
||||
// 'titleBar.activeForeground': '#cccccc',
|
||||
// 'titleBar.border': '#2b2b2b',
|
||||
// 'titleBar.inactiveBackground': '#1f1f1f',
|
||||
// 'titleBar.inactiveForeground': '#9d9d9d',
|
||||
// 'welcomePage.tileBackground': '#2b2b2b'
|
||||
// };
|
||||
|
||||
|
||||
|
||||
export const COLOR_THEME_DARK_INITIAL_COLORS = { // Void changed this to match dark+
|
||||
'activityBar.activeBorder': '#ffffff',
|
||||
'activityBar.background': '#333333',
|
||||
'activityBar.border': '#454545',
|
||||
'activityBar.foreground': '#ffffff',
|
||||
'activityBar.inactiveForeground': '#ffffff66',
|
||||
'editorGroup.border': '#444444',
|
||||
'editorGroupHeader.tabsBackground': '#252526',
|
||||
'editorGroupHeader.tabsBorder': '#252526',
|
||||
'statusBar.background': '#007ACC',
|
||||
'statusBar.border': '#454545',
|
||||
'statusBar.foreground': '#ffffff',
|
||||
'statusBar.noFolderBackground': '#68217A',
|
||||
'tab.activeBackground': '#2D2D2D',
|
||||
'tab.activeBorder': '#ffffff',
|
||||
'tab.activeBorderTop': '#007ACC',
|
||||
'tab.activeForeground': '#ffffff',
|
||||
'tab.border': '#2b2b2b',
|
||||
'textLink.foreground': '#4daafc',
|
||||
'titleBar.activeBackground': '#181818',
|
||||
'titleBar.activeForeground': '#cccccc',
|
||||
'titleBar.border': '#2b2b2b',
|
||||
'titleBar.inactiveBackground': '#1f1f1f',
|
||||
'titleBar.inactiveForeground': '#9d9d9d',
|
||||
'welcomePage.tileBackground': '#2b2b2b'
|
||||
'tab.border': '#252526',
|
||||
'textLink.foreground': '#3794ff',
|
||||
'titleBar.activeBackground': '#3C3C3C',
|
||||
'titleBar.activeForeground': '#CCCCCC',
|
||||
'titleBar.border': '#454545',
|
||||
'titleBar.inactiveBackground': '#2C2C2C',
|
||||
'titleBar.inactiveForeground': '#999999',
|
||||
'welcomePage.tileBackground': '#252526'
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
export const COLOR_THEME_LIGHT_INITIAL_COLORS = {
|
||||
'activityBar.activeBorder': '#005FB8',
|
||||
'activityBar.background': '#f8f8f8',
|
||||
|
||||
@@ -17,7 +17,6 @@ import './browser/workbench.contribution.js';
|
||||
//#region --- Void
|
||||
// Void added this:
|
||||
import './contrib/void/browser/void.contribution.js';
|
||||
import '../platform/void/browser/void.contribution.js';
|
||||
//#endregion
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user